mirror of
https://github.com/agoramachina/claude-exporter.git
synced 2026-07-09 15:59:52 +00:00
Default Model column to original; configurable in Options (v1.9.14)
- Backup filename prefix: claude-exporter-backup-* → claude-database-*
(same timestamp format, still JSON)
- Browse Model column display is now controlled by a new modelDisplay
preference saved in chrome.storage.local. Default is 'original'
(revert of v1.9.12's flip-to-current), with 'current' available as
an opt-in via a new "Model Display" section on the Options page.
- Bounced chats keep the "*" marker in either mode. Tooltip text
adapts: "Originally X" when displaying current, "Now using X" when
displaying original. Bounce signal is preserved regardless of which
model the cell shows.
- getDisplayModel now returns { model, other, otherLabel, bounced }
with the displayed value chosen by the preference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ff9b89ee2
commit
51ceebb71d
12 changed files with 181 additions and 23 deletions
|
|
@ -51,6 +51,7 @@ let modelSnapshots = {}; // Map conversation UUID to { firstSeen, current, ... }
|
|||
let statusFilter = 'all'; // 'all', 'new', 'exported'
|
||||
let dateFormat = 'mdy'; // 'mdy' or 'dmy'
|
||||
let timeFormat = '12h'; // '12h' or '24h'
|
||||
let modelDisplay = 'original'; // 'original' (first-seen) or 'current'
|
||||
|
||||
// Export timestamp storage helpers
|
||||
async function loadExportTimestamps() {
|
||||
|
|
@ -74,20 +75,25 @@ async function loadModelSnapshots() {
|
|||
});
|
||||
}
|
||||
|
||||
// Resolve which model to show for a conversation: the current model from the
|
||||
// snapshot if we have one, otherwise the conversation's reported/inferred
|
||||
// model. Also reports the original (first-seen) model and whether the chat
|
||||
// has since been bounced to a different one — bounced chats get a `*` marker.
|
||||
// Resolve which model to show for a conversation. Honors the modelDisplay
|
||||
// preference ('original' default, or 'current'). When the chat has been
|
||||
// bounced (current differs from first-seen), `bounced` is true and the
|
||||
// `*` marker shows the "other" model in its tooltip.
|
||||
function getDisplayModel(conv) {
|
||||
const snap = modelSnapshots[conv.uuid];
|
||||
if (snap && snap.firstSeen) {
|
||||
const original = snap.firstSeen;
|
||||
const current = snap.current || snap.firstSeen;
|
||||
const bounced = !!snap.current && snap.current !== snap.firstSeen;
|
||||
const useCurrent = modelDisplay === 'current';
|
||||
return {
|
||||
model: snap.current || snap.firstSeen,
|
||||
original: snap.firstSeen,
|
||||
bounced: !!snap.current && snap.current !== snap.firstSeen
|
||||
model: useCurrent ? current : original,
|
||||
other: useCurrent ? original : current,
|
||||
otherLabel: useCurrent ? 'Originally' : 'Now using',
|
||||
bounced
|
||||
};
|
||||
}
|
||||
return { model: conv.model, original: conv.model, bounced: false };
|
||||
return { model: conv.model, other: conv.model, otherLabel: '', bounced: false };
|
||||
}
|
||||
|
||||
async function saveExportTimestamp(conversationId) {
|
||||
|
|
@ -117,6 +123,15 @@ async function loadDateTimePrefs() {
|
|||
});
|
||||
}
|
||||
|
||||
async function loadModelDisplayPref() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.get(['modelDisplay'], (result) => {
|
||||
modelDisplay = result.modelDisplay === 'current' ? 'current' : 'original';
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(dt) {
|
||||
const m = dt.getMonth() + 1;
|
||||
const d = dt.getDate();
|
||||
|
|
@ -145,6 +160,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
await loadExportTimestamps();
|
||||
await loadModelSnapshots();
|
||||
await loadDateTimePrefs();
|
||||
await loadModelDisplayPref();
|
||||
const elapsed = Date.now() - loadingStart;
|
||||
if (elapsed < 1000) await new Promise(r => setTimeout(r, 1000 - elapsed));
|
||||
const loadingText = document.getElementById('loadingText');
|
||||
|
|
@ -444,7 +460,7 @@ function displayConversations() {
|
|||
<td>
|
||||
<span class="model-badge ${modelBadgeClass}">
|
||||
${escapeHtml(formatModelName(modelInfo.model))}
|
||||
</span>${modelInfo.bounced ? `<span class="model-bounced" title="Originally ${escapeHtml(formatModelName(modelInfo.original))}">*</span>` : ''}
|
||||
</span>${modelInfo.bounced ? `<span class="model-bounced" title="${modelInfo.otherLabel} ${escapeHtml(formatModelName(modelInfo.other))}">*</span>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Claude Exporter Beta",
|
||||
"version": "1.9.13",
|
||||
"version": "1.9.14",
|
||||
"description": "Export conversations and artifacts from Claude.ai",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
|
|
|||
|
|
@ -123,6 +123,32 @@
|
|||
.date-time-grid label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.radio-option {
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
border-radius: 5px;
|
||||
margin-top: 8px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-body);
|
||||
font-weight: normal;
|
||||
}
|
||||
.radio-option:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.radio-option input {
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.radio-option strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.radio-option .radio-desc {
|
||||
display: block;
|
||||
margin: 4px 0 0 22px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
|
|
@ -235,6 +261,22 @@
|
|||
<div id="dateTimeStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Model Display</h3>
|
||||
<p>Which model name to show in the browse view's Model column. Bounced chats (where the model has changed) get a small "*" marker either way — the tooltip shows the other model.</p>
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="modelDisplay" value="original">
|
||||
<strong>Original</strong>
|
||||
<span class="radio-desc">The model that first ran the conversation (first-seen). Preserves the original even after the chat gets bounced to a newer model.</span>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="modelDisplay" value="current">
|
||||
<strong>Current</strong>
|
||||
<span class="radio-desc">The model the conversation uses right now. Reflects what will run if you reopen the chat.</span>
|
||||
</label>
|
||||
<div id="modelDisplayStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<script src="utils.js"></script>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,24 @@ document.getElementById('timeFormatSelect').addEventListener('change', (e) => {
|
|||
});
|
||||
});
|
||||
|
||||
// Model display preference (browse view's Model column)
|
||||
function loadModelDisplayPref() {
|
||||
chrome.storage.local.get(['modelDisplay'], (result) => {
|
||||
const value = result.modelDisplay === 'current' ? 'current' : 'original';
|
||||
const radio = document.querySelector(`input[name="modelDisplay"][value="${value}"]`);
|
||||
if (radio) radio.checked = true;
|
||||
});
|
||||
}
|
||||
loadModelDisplayPref();
|
||||
|
||||
document.querySelectorAll('input[name="modelDisplay"]').forEach((radio) => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
chrome.storage.local.set({ modelDisplay: e.target.value }, () => {
|
||||
showStatus('modelDisplayStatus', 'Model display preference saved. Reload the browse page to see the change.', 'success');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
function showStatus(elementId, message, type) {
|
||||
const statusEl = document.getElementById(elementId);
|
||||
|
|
|
|||
|
|
@ -694,7 +694,7 @@ function backupExtensionData(onComplete) {
|
|||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `claude-exporter-backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
|
||||
a.download = `claude-database-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
## [1.9.14]
|
||||
|
||||
- Backup filename changed from `claude-exporter-backup-(timestamp).json` to `claude-database-(timestamp).json`
|
||||
- Browse table Model column display is now configurable. Defaults to **Original** (first-seen model, the v1.9.4 behavior); a new "Model Display" section in Options lets you switch to **Current** (v1.9.12 behavior)
|
||||
- Bounced chats keep the `*` marker in either mode; the tooltip now shows "Originally X" when displaying current, and "Now using X" when displaying original
|
||||
|
||||
## [1.9.13]
|
||||
|
||||
- Backup/Restore renamed throughout to **Export Backup** / **Import Backup** (options page buttons + browse dropdown items)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
- **Track model changes per conversation**
|
||||
- **Phase 1 capture SHIPPED (v1.9.3)** — `recordModelSnapshots()` in `content.js` writes `modelSnapshots` to `chrome.storage.local` every time the conversation list is fetched (browse page load or popup "Export All"), not just on export. Stores `{firstSeen, firstSeenAt, current, currentAt, history[]}` per conversation UUID; raw API model only, never an inferred guess.
|
||||
- **Browse-table display SHIPPED (v1.9.4, revised v1.9.12)** — Model column shows the **current** model via `getDisplayModel()`, with a `*` asterisk marker + "Originally X" tooltip when the chat has been bounced. Falls back to current/inferred when no snapshot exists. (v1.9.4 originally showed first-seen with a `→` marker; v1.9.12 flipped to current so the cell reflects what model the chat will actually use on reopen.)
|
||||
- **Browse-table display SHIPPED (v1.9.4, revised v1.9.12, configurable v1.9.14)** — Model column shows either the original (first-seen) or current model via `getDisplayModel()`, controlled by the `modelDisplay` preference (default 'original'). Bounced chats get a `*` marker with a tooltip showing the "other" model ("Originally X" when displaying current, "Now using X" when displaying original). Options page "Model Display" section lets users switch.
|
||||
- Still pending: surface the snapshot in JSON exports (sidecar or inline field); optionally a dedicated "current model" column or filter for bounced chats
|
||||
- `conversation.model` from the API is the *current* model only — when chats get bounced (deprecation, guardrails kicking to Sonnet 4, etc.) the original model is lost
|
||||
- Symptom: chats created before Sonnet 4.5 existed now show "Sonnet 4.5" because that's their current default
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ let modelSnapshots = {}; // Map conversation UUID to { firstSeen, current, ... }
|
|||
let statusFilter = 'all'; // 'all', 'new', 'exported'
|
||||
let dateFormat = 'mdy'; // 'mdy' or 'dmy'
|
||||
let timeFormat = '12h'; // '12h' or '24h'
|
||||
let modelDisplay = 'original'; // 'original' (first-seen) or 'current'
|
||||
|
||||
// Export timestamp storage helpers
|
||||
async function loadExportTimestamps() {
|
||||
|
|
@ -74,20 +75,25 @@ async function loadModelSnapshots() {
|
|||
});
|
||||
}
|
||||
|
||||
// Resolve which model to show for a conversation: the current model from the
|
||||
// snapshot if we have one, otherwise the conversation's reported/inferred
|
||||
// model. Also reports the original (first-seen) model and whether the chat
|
||||
// has since been bounced to a different one — bounced chats get a `*` marker.
|
||||
// Resolve which model to show for a conversation. Honors the modelDisplay
|
||||
// preference ('original' default, or 'current'). When the chat has been
|
||||
// bounced (current differs from first-seen), `bounced` is true and the
|
||||
// `*` marker shows the "other" model in its tooltip.
|
||||
function getDisplayModel(conv) {
|
||||
const snap = modelSnapshots[conv.uuid];
|
||||
if (snap && snap.firstSeen) {
|
||||
const original = snap.firstSeen;
|
||||
const current = snap.current || snap.firstSeen;
|
||||
const bounced = !!snap.current && snap.current !== snap.firstSeen;
|
||||
const useCurrent = modelDisplay === 'current';
|
||||
return {
|
||||
model: snap.current || snap.firstSeen,
|
||||
original: snap.firstSeen,
|
||||
bounced: !!snap.current && snap.current !== snap.firstSeen
|
||||
model: useCurrent ? current : original,
|
||||
other: useCurrent ? original : current,
|
||||
otherLabel: useCurrent ? 'Originally' : 'Now using',
|
||||
bounced
|
||||
};
|
||||
}
|
||||
return { model: conv.model, original: conv.model, bounced: false };
|
||||
return { model: conv.model, other: conv.model, otherLabel: '', bounced: false };
|
||||
}
|
||||
|
||||
async function saveExportTimestamp(conversationId) {
|
||||
|
|
@ -117,6 +123,15 @@ async function loadDateTimePrefs() {
|
|||
});
|
||||
}
|
||||
|
||||
async function loadModelDisplayPref() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.get(['modelDisplay'], (result) => {
|
||||
modelDisplay = result.modelDisplay === 'current' ? 'current' : 'original';
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(dt) {
|
||||
const m = dt.getMonth() + 1;
|
||||
const d = dt.getDate();
|
||||
|
|
@ -145,6 +160,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
await loadExportTimestamps();
|
||||
await loadModelSnapshots();
|
||||
await loadDateTimePrefs();
|
||||
await loadModelDisplayPref();
|
||||
const elapsed = Date.now() - loadingStart;
|
||||
if (elapsed < 1000) await new Promise(r => setTimeout(r, 1000 - elapsed));
|
||||
const loadingText = document.getElementById('loadingText');
|
||||
|
|
@ -444,7 +460,7 @@ function displayConversations() {
|
|||
<td>
|
||||
<span class="model-badge ${modelBadgeClass}">
|
||||
${escapeHtml(formatModelName(modelInfo.model))}
|
||||
</span>${modelInfo.bounced ? `<span class="model-bounced" title="Originally ${escapeHtml(formatModelName(modelInfo.original))}">*</span>` : ''}
|
||||
</span>${modelInfo.bounced ? `<span class="model-bounced" title="${modelInfo.otherLabel} ${escapeHtml(formatModelName(modelInfo.other))}">*</span>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Claude Exporter Beta",
|
||||
"version": "1.9.13",
|
||||
"version": "1.9.14",
|
||||
"description": "Export conversations and artifacts from Claude.ai",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
|
|
|||
|
|
@ -123,6 +123,32 @@
|
|||
.date-time-grid label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.radio-option {
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
border-radius: 5px;
|
||||
margin-top: 8px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-body);
|
||||
font-weight: normal;
|
||||
}
|
||||
.radio-option:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.radio-option input {
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.radio-option strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.radio-option .radio-desc {
|
||||
display: block;
|
||||
margin: 4px 0 0 22px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
|
|
@ -235,6 +261,22 @@
|
|||
<div id="dateTimeStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Model Display</h3>
|
||||
<p>Which model name to show in the browse view's Model column. Bounced chats (where the model has changed) get a small "*" marker either way — the tooltip shows the other model.</p>
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="modelDisplay" value="original">
|
||||
<strong>Original</strong>
|
||||
<span class="radio-desc">The model that first ran the conversation (first-seen). Preserves the original even after the chat gets bounced to a newer model.</span>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="modelDisplay" value="current">
|
||||
<strong>Current</strong>
|
||||
<span class="radio-desc">The model the conversation uses right now. Reflects what will run if you reopen the chat.</span>
|
||||
</label>
|
||||
<div id="modelDisplayStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<script src="utils.js"></script>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,24 @@ document.getElementById('timeFormatSelect').addEventListener('change', (e) => {
|
|||
});
|
||||
});
|
||||
|
||||
// Model display preference (browse view's Model column)
|
||||
function loadModelDisplayPref() {
|
||||
chrome.storage.local.get(['modelDisplay'], (result) => {
|
||||
const value = result.modelDisplay === 'current' ? 'current' : 'original';
|
||||
const radio = document.querySelector(`input[name="modelDisplay"][value="${value}"]`);
|
||||
if (radio) radio.checked = true;
|
||||
});
|
||||
}
|
||||
loadModelDisplayPref();
|
||||
|
||||
document.querySelectorAll('input[name="modelDisplay"]').forEach((radio) => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
chrome.storage.local.set({ modelDisplay: e.target.value }, () => {
|
||||
showStatus('modelDisplayStatus', 'Model display preference saved. Reload the browse page to see the change.', 'success');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
function showStatus(elementId, message, type) {
|
||||
const statusEl = document.getElementById(elementId);
|
||||
|
|
|
|||
|
|
@ -694,7 +694,7 @@ function backupExtensionData(onComplete) {
|
|||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `claude-exporter-backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
|
||||
a.download = `claude-database-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue