mirror of
https://github.com/bal-spec/sillytavern-character-memory.git
synced 2026-05-02 13:41:12 +00:00
829 lines
43 KiB
Markdown
829 lines
43 KiB
Markdown
# Panel Restructure + Card-Based Editor Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Restructure the extension panel to use top-level tabs (Main, Consolidate, Batch Extract, Settings, Log) and replace the raw-text consolidation editor with a card-based editor matching the left pane's visual style.
|
|
|
|
**Architecture:** Promote the existing sub-tab system to the top level of the panel. Move consolidation config into its own tab. Rewrite the consolidation dialog's right pane from a textarea to an interactive card editor backed by an in-memory block array. Reuse existing `charMemory_card` styling and `parseMemories`/`serializeMemories` for data flow.
|
|
|
|
**Tech Stack:** jQuery (ST convention), `callGenericPopup` for the consolidation popup, existing `charMemory_tab` CSS system.
|
|
|
|
**Design doc:** `docs/plans/2026-02-16-panel-restructure-design.md`
|
|
|
|
---
|
|
|
|
### Task 1: Restructure settings.html to top-level tabs
|
|
|
|
**Files:**
|
|
- Modify: `settings.html` (complete restructure)
|
|
|
|
This is the biggest single change. The entire panel HTML gets reorganized from drawers to tabs. No JS logic changes — just HTML structure.
|
|
|
|
**Step 1: Rewrite settings.html**
|
|
|
|
Replace the entire contents of `settings.html` with this structure. The content within each section is moved from the existing drawers — nothing is invented, just relocated.
|
|
|
|
```html
|
|
<div class="charMemory_settings">
|
|
<div class="inline-drawer">
|
|
<div class="inline-drawer-toggle inline-drawer-header">
|
|
<b>Character Memory</b>
|
|
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
|
</div>
|
|
<div class="inline-drawer-content">
|
|
|
|
<!-- Stats bar — always visible -->
|
|
<div class="charMemory_statsBar">
|
|
<div class="charMemory_statItem" title="The Data Bank file where memories are stored for this character">
|
|
<i class="fa-solid fa-file-lines fa-sm"></i>
|
|
<span id="charMemory_statFile">No character</span>
|
|
</div>
|
|
<div class="charMemory_statItem" title="Total number of individual memory bullets stored">
|
|
<i class="fa-solid fa-brain fa-sm"></i>
|
|
<span id="charMemory_statCount">0 memories</span>
|
|
</div>
|
|
<div class="charMemory_statItem" title="New messages since last extraction / auto-extraction threshold">
|
|
<i class="fa-solid fa-arrows-rotate fa-sm"></i>
|
|
<span id="charMemory_statProgress">0/10 msgs</span>
|
|
</div>
|
|
<div class="charMemory_statItem" title="Time remaining before the next auto-extraction is allowed">
|
|
<i class="fa-solid fa-clock fa-sm"></i>
|
|
<span id="charMemory_statCooldown">Ready</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="charMemory_statusRow">
|
|
<label class="checkbox_label" for="charMemory_enabled" title="When enabled, memories are extracted automatically after a set number of new messages">
|
|
<input type="checkbox" id="charMemory_enabled" />
|
|
<span>Enable automatic extraction</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Top-level tabs -->
|
|
<div class="charMemory_tabs">
|
|
<button class="charMemory_tab active" data-tab="main">Main</button>
|
|
<button class="charMemory_tab" data-tab="consolidate">Consolidate</button>
|
|
<button class="charMemory_tab" data-tab="batch">Batch Extract</button>
|
|
<button class="charMemory_tab" data-tab="settings">Settings</button>
|
|
<button class="charMemory_tab" data-tab="log">Log</button>
|
|
</div>
|
|
|
|
<!-- Main tab (default) -->
|
|
<div class="charMemory_tabContent" id="charMemory_tabMain">
|
|
<div class="charMemory_buttonRow">
|
|
<input type="button" id="charMemory_extractNow" class="menu_button" value="Extract Now" title="Extract memories from unprocessed messages. If all messages have been processed, use 'Reset Extraction State' first to re-read from the beginning." />
|
|
<input type="button" id="charMemory_manageMemories" class="menu_button" value="View / Edit" title="Browse, edit, and delete individual stored memories" />
|
|
</div>
|
|
<hr class="charMemory_separator" />
|
|
<div class="charMemory_buttonRow">
|
|
<input type="button" id="charMemory_refreshDiag" class="menu_button" value="Refresh Diagnostics" title="Capture current diagnostics (lorebook entries, extension prompts, memory file status)" />
|
|
</div>
|
|
<div id="charMemory_diagnosticsContent" class="charMemory_diagnosticsContent">
|
|
<div class="charMemory_diagEmpty">Click "Refresh Diagnostics" after a generation to see diagnostics.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Consolidate tab -->
|
|
<div class="charMemory_tabContent" id="charMemory_tabConsolidate" style="display:none;">
|
|
<div class="charMemory_promptSection">
|
|
<label for="charMemory_consolidationStrategy">
|
|
<small>Consolidation strategy</small>
|
|
</label>
|
|
<select id="charMemory_consolidationStrategy" class="text_pole">
|
|
<option value="conservative">Conservative — only merge near-exact duplicates</option>
|
|
<option value="balanced">Balanced — merge duplicates & related facts (default)</option>
|
|
<option value="aggressive">Aggressive — compress heavily, summarize themes</option>
|
|
<option value="custom">Custom prompt</option>
|
|
</select>
|
|
<textarea id="charMemory_consolidationPrompt" class="text_pole textarea_compact" rows="6" placeholder="Enter custom consolidation prompt..." style="display:none;"></textarea>
|
|
<small id="charMemory_consolidationPreview" class="charMemory_helperText" style="font-style:italic;"></small>
|
|
</div>
|
|
<div class="charMemory_buttonRow">
|
|
<input type="button" id="charMemory_consolidate" class="menu_button" value="Consolidate" title="Use the LLM to merge duplicate and related memories into fewer, cleaner entries" />
|
|
<input type="button" id="charMemory_undoConsolidate" class="menu_button" value="Undo Consolidation" title="Restore memories from before the last consolidation (session only)" disabled />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Batch Extract tab -->
|
|
<div class="charMemory_tabContent" id="charMemory_tabBatch" style="display:none;">
|
|
<div class="charMemory_buttonRow">
|
|
<input type="button" id="charMemory_batchRefresh" class="menu_button" value="Refresh" title="Load chat list for this character" />
|
|
<input type="button" id="charMemory_batchExtract" class="menu_button" value="Extract Selected" title="Run extraction on all selected chats" disabled />
|
|
<input type="button" id="charMemory_batchStop" class="menu_button" value="Stop" title="Cancel batch extraction" style="display:none;" />
|
|
</div>
|
|
<div class="charMemory_batchSelectRow">
|
|
<label class="checkbox_label">
|
|
<input type="checkbox" id="charMemory_batchSelectAll" />
|
|
<small>Select all</small>
|
|
</label>
|
|
</div>
|
|
<div id="charMemory_batchProgress" class="charMemory_batchProgress" style="display:none;">
|
|
<div class="charMemory_batchProgressText"></div>
|
|
<div class="charMemory_batchProgressBar"><div class="charMemory_batchProgressFill"></div></div>
|
|
</div>
|
|
<div id="charMemory_batchChatList" class="charMemory_batchChatList">
|
|
<div class="charMemory_diagEmpty">Click "Refresh" to load chats.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings tab -->
|
|
<div class="charMemory_tabContent" id="charMemory_tabSettings" style="display:none;">
|
|
|
|
<!-- LLM Used for Extraction -->
|
|
<div class="charMemory_statusRow">
|
|
<label for="charMemory_source" title="Which LLM to use for memory extraction and consolidation">
|
|
<small>LLM Used for Extraction</small>
|
|
</label>
|
|
<select id="charMemory_source" class="text_pole">
|
|
<option value="provider">Dedicated API (recommended)</option>
|
|
<option value="webllm">WebLLM (browser-local)</option>
|
|
<option value="main_llm">Main LLM</option>
|
|
</select>
|
|
<small class="charMemory_helperText"><b>Dedicated API is recommended.</b> Main LLM pollutes the extraction prompt with chat context, system prompts, and other instructions that degrade memory quality.</small>
|
|
|
|
<div id="charMemory_providerSettings" style="display:none;">
|
|
<div class="charMemory_statusRow">
|
|
<label><small>Provider</small></label>
|
|
<select id="charMemory_providerSelect" class="text_pole">
|
|
</select>
|
|
</div>
|
|
<div class="charMemory_statusRow" id="charMemory_providerApiKeyRow">
|
|
<label><small>API Key <a id="charMemory_providerHelpLink" href="#" target="_blank" style="font-size:0.85em;">(get key)</a></small></label>
|
|
<div style="display:flex;gap:5px;align-items:center;">
|
|
<input type="password" id="charMemory_providerApiKey" class="text_pole" placeholder="Enter API key" style="flex:1;" />
|
|
<button type="button" id="charMemory_providerApiKeyReveal" class="menu_button" title="Show/hide API key" style="padding:3px 8px;">
|
|
<i class="fa-solid fa-eye fa-sm"></i>
|
|
</button>
|
|
<input type="button" id="charMemory_providerConnect" class="menu_button" value="Connect" title="Fetch available models using your API key" />
|
|
</div>
|
|
</div>
|
|
<small id="charMemory_providerConnectStatus" class="charMemory_helperText" style="display:none;"></small>
|
|
<div class="charMemory_statusRow" id="charMemory_providerBaseUrlRow" style="display:none;">
|
|
<label><small>Base URL</small></label>
|
|
<input type="text" id="charMemory_providerBaseUrl" class="text_pole" placeholder="https://your-server.com/v1" />
|
|
</div>
|
|
<div class="charMemory_statusRow" id="charMemory_providerModelDropdownRow">
|
|
<label><small>Model</small></label>
|
|
<div id="charMemory_nanogptFilters" style="display:none;">
|
|
<div class="charMemory_filterRow" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:4px;">
|
|
<label class="checkbox_label"><input type="checkbox" id="charMemory_nanogptFilterSub" /> <small>Subscription</small></label>
|
|
<label class="checkbox_label"><input type="checkbox" id="charMemory_nanogptFilterOS" /> <small>Open Source</small></label>
|
|
<label class="checkbox_label"><input type="checkbox" id="charMemory_nanogptFilterRP" /> <small>Roleplay</small></label>
|
|
<label class="checkbox_label"><input type="checkbox" id="charMemory_nanogptFilterReasoning" /> <small>Reasoning</small></label>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:5px;align-items:center;">
|
|
<select id="charMemory_providerModel" class="text_pole" style="flex:1;">
|
|
<option value="">-- Select model --</option>
|
|
</select>
|
|
<input type="button" id="charMemory_providerRefreshModels" class="menu_button" value="↻" title="Refresh model list" />
|
|
</div>
|
|
<small id="charMemory_providerModelInfo" class="charMemory_helperText"></small>
|
|
</div>
|
|
<div class="charMemory_statusRow" id="charMemory_providerTestRow">
|
|
<div style="display:flex;gap:5px;align-items:center;">
|
|
<input type="button" id="charMemory_providerTest" class="menu_button" value="Test Model" title="Send a test prompt to the selected model and verify it responds correctly" />
|
|
</div>
|
|
<small id="charMemory_providerTestStatus" class="charMemory_helperText" style="display:none;"></small>
|
|
</div>
|
|
<div class="charMemory_statusRow" id="charMemory_providerModelInputRow" style="display:none;">
|
|
<label><small>Model ID</small></label>
|
|
<input type="text" id="charMemory_providerModelInput" class="text_pole" placeholder="Enter model identifier" />
|
|
<small class="charMemory_helperText">Enter the model ID manually (e.g. claude-sonnet-4-5-20250929).</small>
|
|
</div>
|
|
<div class="charMemory_statusRow">
|
|
<label><small>System prompt (optional)</small></label>
|
|
<textarea id="charMemory_providerSystemPrompt" class="text_pole" rows="3" placeholder="Override the default system prompt. Leave blank for default."></textarea>
|
|
<small class="charMemory_helperText">Prepended to extraction/consolidation calls. Use for jailbreaks or custom instructions.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Auto-Extraction -->
|
|
<hr class="charMemory_separator" />
|
|
<small><b>Auto-Extraction</b></small>
|
|
|
|
<div class="charMemory_sliderRow">
|
|
<label title="How many new messages trigger an automatic extraction.">
|
|
<small>Extract after every N messages</small>
|
|
</label>
|
|
<input class="neo-range-slider" type="range" id="charMemory_interval" min="3" max="100" step="1" value="20" />
|
|
<div class="wide100p">
|
|
<input class="neo-range-input" type="number" min="3" max="100" step="1"
|
|
data-for="charMemory_interval" id="charMemory_intervalCounter" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="charMemory_sliderRow">
|
|
<label title="Minimum time between auto-extractions, even if the message threshold is met.">
|
|
<small>Minimum wait between extractions (min)</small>
|
|
</label>
|
|
<input class="neo-range-slider" type="range" id="charMemory_minCooldown" min="0" max="30" step="1" value="10" />
|
|
<div class="wide100p">
|
|
<input class="neo-range-input" type="number" min="0" max="30" step="1"
|
|
data-for="charMemory_minCooldown" id="charMemory_minCooldownCounter" />
|
|
</div>
|
|
</div>
|
|
|
|
<small class="charMemory_helperText">These settings only affect automatic extraction. Manual extraction and batch extraction ignore them.</small>
|
|
|
|
<!-- Extraction Settings -->
|
|
<hr class="charMemory_separator" />
|
|
<small><b>Extraction Settings</b></small>
|
|
|
|
<div class="charMemory_sliderRow">
|
|
<label title="How many messages to include in each LLM call.">
|
|
<small>Messages per LLM call</small>
|
|
</label>
|
|
<input class="neo-range-slider" type="range" id="charMemory_maxMessages" min="10" max="200" step="1" value="50" />
|
|
<div class="wide100p">
|
|
<input class="neo-range-input" type="number" min="10" max="200" step="1"
|
|
data-for="charMemory_maxMessages" id="charMemory_maxMessagesCounter" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="charMemory_sliderRow">
|
|
<label title="Maximum tokens the LLM can use for its response.">
|
|
<small>Max response length</small>
|
|
</label>
|
|
<input class="neo-range-slider" type="range" id="charMemory_responseLength" min="100" max="4000" step="50" value="1000" />
|
|
<div class="wide100p">
|
|
<input class="neo-range-input" type="number" min="100" max="4000" step="50"
|
|
data-for="charMemory_responseLength" id="charMemory_responseLengthCounter" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Storage -->
|
|
<hr class="charMemory_separator" />
|
|
|
|
<div class="charMemory_statusRow">
|
|
<label class="checkbox_label" for="charMemory_perChat" title="When enabled, each chat gets its own memory file.">
|
|
<input type="checkbox" id="charMemory_perChat" />
|
|
<span>Separate memories per chat</span>
|
|
</label>
|
|
<small class="charMemory_helperText">Each conversation gets its own memory file instead of sharing one per character.</small>
|
|
</div>
|
|
|
|
<div class="charMemory_statusRow">
|
|
<label for="charMemory_fileName" title="Override the auto-generated file name.">
|
|
<small>File name override</small>
|
|
</label>
|
|
<input type="text" id="charMemory_fileName" class="text_pole" placeholder="(auto-generated from character name)" />
|
|
<small class="charMemory_helperText">Leave blank for auto-naming. Set a custom name to override.</small>
|
|
</div>
|
|
|
|
<!-- Extraction Prompt -->
|
|
<hr class="charMemory_separator" />
|
|
|
|
<div class="charMemory_promptSection">
|
|
<label for="charMemory_extractionPrompt" title="The prompt sent to the LLM for memory extraction. Uses {{charName}}, {{charCard}}, {{existingMemories}}, {{recentMessages}}, {{char}}, and {{user}} placeholders.">
|
|
<small>Extraction prompt</small>
|
|
</label>
|
|
<textarea id="charMemory_extractionPrompt" class="text_pole textarea_compact" rows="8" placeholder="Enter extraction prompt..."></textarea>
|
|
<input type="button" id="charMemory_restorePrompt" class="menu_button" value="Restore Default Prompt" title="Replace the current prompt with the built-in default" />
|
|
</div>
|
|
|
|
<!-- Reset / Clear -->
|
|
<hr class="charMemory_separator" />
|
|
|
|
<div class="charMemory_statusRow">
|
|
<input type="button" id="charMemory_resetTracking" class="menu_button" value="Reset Extraction State" title="Reset extraction tracking for the current character's chats" />
|
|
<small class="charMemory_helperText">Resets extraction tracking for the current character. Use before 'Extract Now' or 'Batch Extract' to re-process from the beginning.</small>
|
|
<input type="button" id="charMemory_resetExtraction" class="menu_button charMemory_dangerBtn" value="Clear All Memories" title="Delete the memory file and reset extraction state for the current character." />
|
|
<small class="charMemory_helperText">Deletes the memory file and resets extraction tracking for the current character. This cannot be undone.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Log tab -->
|
|
<div class="charMemory_tabContent" id="charMemory_tabLog" style="display:none;">
|
|
<div class="charMemory_buttonRow">
|
|
<input type="button" id="charMemory_clearLog" class="menu_button" value="Clear" title="Clear the activity log" />
|
|
<input type="button" id="charMemory_saveLog" class="menu_button" value="Save Log" title="Download the activity log as a text file" />
|
|
<label class="checkbox_label" for="charMemory_verboseLog" title="Show full LLM prompts and responses in the activity log">
|
|
<input type="checkbox" id="charMemory_verboseLog" />
|
|
<small>Verbose</small>
|
|
</label>
|
|
</div>
|
|
<div id="charMemory_activityLog" class="charMemory_activityLog" style="max-height:300px;overflow-y:auto;font-size:0.85em;font-family:monospace;">
|
|
<div class="charMemory_diagEmpty">No activity yet.</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
Key changes from the original:
|
|
- The top-level button row (Extract Now, View/Edit, Consolidate, Undo) is split: Extract+View go to Main tab, Consolidate+Undo go to Consolidate tab
|
|
- The Settings drawer becomes the Settings tab (no longer a nested drawer)
|
|
- The Tools & Diagnostics drawer is removed; its contents are split: Batch Extract and Log become their own tabs, Diagnostics moves into Main tab
|
|
- Consolidation strategy/prompt controls move from Settings to the Consolidate tab
|
|
- All element IDs are unchanged so existing JS event handlers continue to work
|
|
|
|
**Step 2: Commit**
|
|
|
|
```bash
|
|
git add settings.html
|
|
git commit -m "refactor: restructure panel to top-level tab layout"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Update tab switching logic in index.js
|
|
|
|
**Files:**
|
|
- Modify: `index.js:2970-2979` (tab switching handler)
|
|
|
|
The existing tab handler uses a convention: `data-tab="batch"` maps to `#charMemory_tabBatch`. The new tabs follow the same convention (`main` -> `tabMain`, `consolidate` -> `tabConsolidate`, etc.), so the handler logic stays the same. But we need to update the old drawer-based handler since the Tools drawer is gone.
|
|
|
|
**Step 1: Update the tab handler**
|
|
|
|
The current handler at line 2971 already works generically:
|
|
|
|
```javascript
|
|
$('.charMemory_tab').off('click').on('click', function () {
|
|
const tab = $(this).data('tab');
|
|
$('.charMemory_tab').removeClass('active');
|
|
$(this).addClass('active');
|
|
$('.charMemory_tabContent').hide();
|
|
const capName = tab.charAt(0).toUpperCase() + tab.slice(1);
|
|
$(`#charMemory_tab${capName}`).show();
|
|
if (tab === 'batch') loadBatchChatList();
|
|
});
|
|
```
|
|
|
|
This already handles the new tabs correctly (the capitalization convention works for `main` -> `Main`, `consolidate` -> `Consolidate`, `settings` -> `Settings`, etc.). **No changes needed to the tab handler itself.**
|
|
|
|
However, the old `Tools & Diagnostics` drawer toggle no longer exists in the HTML. Check if there are any event listeners referencing it that need removal. Search for references to the old drawer structure.
|
|
|
|
**Step 2: Remove the old drawer-specific listener if any**
|
|
|
|
Search `index.js` for any `inline-drawer` or `Tools` references specific to the old Tools drawer. The generic SillyTavern drawer toggle is handled by ST core, not by our code, so there likely isn't anything to remove. Verify this.
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add index.js
|
|
git commit -m "refactor: verify tab switching works with new layout"
|
|
```
|
|
|
|
If no JS changes were needed, skip this commit.
|
|
|
|
---
|
|
|
|
### Task 3: Rewrite buildConsolidationDialog for card-based editor
|
|
|
|
**Files:**
|
|
- Modify: `index.js:2366-2410` (replace `buildConsolidationDialog` and `countConsolidatedText`)
|
|
- Modify: `style.css` (add editor card styles, replace textarea styles)
|
|
|
|
This is the core UX improvement. Replace the textarea-based right pane with a card-based editor.
|
|
|
|
**Step 1: Replace `buildConsolidationDialog` and `countConsolidatedText`**
|
|
|
|
Replace lines 2366-2410 with:
|
|
|
|
```javascript
|
|
function buildConsolidationDialog(beforeBlocks, beforeCount, consolidatedBlocks) {
|
|
const renderReadOnlyCards = (blocks) => {
|
|
return blocks.map(b => {
|
|
const bullets = b.bullets.map(bullet => `<li>${escapeHtml(bullet)}</li>`).join('');
|
|
return `<div class="charMemory_card">
|
|
<div class="charMemory_cardHeader"><strong>${escapeHtml(b.chat)}</strong> <span class="charMemory_cardDate">${escapeHtml(b.date)}</span></div>
|
|
<ul>${bullets}</ul>
|
|
</div>`;
|
|
}).join('');
|
|
};
|
|
|
|
const renderEditableCards = (blocks) => {
|
|
return blocks.map((b, bi) => {
|
|
const bullets = b.bullets.map((bullet, bui) =>
|
|
`<div class="charMemory_editorBulletRow" data-block="${bi}" data-bullet="${bui}">
|
|
<span class="charMemory_editorDash">-</span>
|
|
<input type="text" class="charMemory_editorBulletInput" value="${escapeHtml(bullet)}" data-block="${bi}" data-bullet="${bui}" />
|
|
<button class="charMemory_editorDeleteBullet menu_button menu_button_icon" data-block="${bi}" data-bullet="${bui}" title="Delete memory"><i class="fa-solid fa-trash fa-xs"></i></button>
|
|
</div>`
|
|
).join('');
|
|
return `<div class="charMemory_card charMemory_editorCard" data-block="${bi}">
|
|
<div class="charMemory_cardHeader">
|
|
<span class="charMemory_cardTitle">${escapeHtml(b.chat)}</span>
|
|
<span class="charMemory_cardTimestamp">${escapeHtml(b.date)}</span>
|
|
<span class="charMemory_cardActions">
|
|
<button class="charMemory_editorDeleteBlock menu_button menu_button_icon" data-block="${bi}" title="Delete block"><i class="fa-solid fa-trash"></i></button>
|
|
</span>
|
|
</div>
|
|
<div class="charMemory_editorBullets">${bullets}</div>
|
|
<button class="charMemory_editorAddBullet menu_button" data-block="${bi}"><i class="fa-solid fa-plus fa-xs"></i> Add memory</button>
|
|
</div>`;
|
|
}).join('');
|
|
};
|
|
|
|
const afterCount = countBlocksBullets(consolidatedBlocks);
|
|
|
|
return `<div class="charMemory_consolidationDialog">
|
|
<div class="charMemory_consolidationStats" id="charMemory_consolidationStats">
|
|
Original: ${beforeCount} memories in ${beforeBlocks.length} blocks → Consolidated: <span id="charMemory_afterCount">${afterCount}</span> memories
|
|
</div>
|
|
<div class="charMemory_consolidationToolbar">
|
|
<select id="charMemory_consolidationDialogStrategy" class="text_pole" style="max-width:200px;">
|
|
${Object.entries(CONSOLIDATION_PRESETS).filter(([k]) => k !== 'custom').map(([k, v]) =>
|
|
`<option value="${k}">${escapeHtml(v.name)}</option>`
|
|
).join('')}
|
|
<option value="custom">Custom</option>
|
|
</select>
|
|
<input type="button" id="charMemory_rerunConsolidation" class="menu_button" value="Re-run" title="Send original memories to the LLM again with current strategy" />
|
|
<input type="button" id="charMemory_undoRerun" class="menu_button" value="Undo" title="Revert to previous consolidated version" disabled />
|
|
<span id="charMemory_rerunSpinner" style="display:none;">Working...</span>
|
|
</div>
|
|
<div class="charMemory_consolidationPanes">
|
|
<div class="charMemory_consolidationPane">
|
|
<h4>Original</h4>
|
|
<div class="charMemory_consolidationContent">${renderReadOnlyCards(beforeBlocks)}</div>
|
|
</div>
|
|
<div class="charMemory_consolidationPane">
|
|
<h4>Consolidated</h4>
|
|
<div class="charMemory_consolidationContent" id="charMemory_editorPane">${renderEditableCards(consolidatedBlocks)}</div>
|
|
<button class="charMemory_editorAddBlock menu_button" id="charMemory_editorAddBlock"><i class="fa-solid fa-plus fa-xs"></i> Add Block</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
function countBlocksBullets(blocks) {
|
|
return blocks.reduce((sum, b) => sum + b.bullets.length, 0);
|
|
}
|
|
```
|
|
|
|
Key changes:
|
|
- Third parameter changes from `consolidatedText` (string) to `consolidatedBlocks` (array of block objects)
|
|
- Right pane renders editable cards with text inputs instead of a textarea
|
|
- Each bullet has an input field + delete button
|
|
- Each block has a delete-block button and "Add memory" button
|
|
- "Add Block" button at the bottom of the right pane
|
|
- `countConsolidatedText` (text-based) replaced with `countBlocksBullets` (block-based)
|
|
- Toolbar moved above the panes (between stats and panes) for better visibility
|
|
|
|
**Step 2: Add CSS for editor cards**
|
|
|
|
In `style.css`, replace the `.charMemory_consolidationEditor` block (the textarea styles, lines 405-416) with:
|
|
|
|
```css
|
|
.charMemory_editorBulletRow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.charMemory_editorDash {
|
|
flex-shrink: 0;
|
|
font-size: 0.85em;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.charMemory_editorBulletInput {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 3px 6px;
|
|
font-size: 0.85em;
|
|
background: var(--SmartThemeBlurTintColor, rgba(0, 0, 0, 0.05));
|
|
color: var(--SmartThemeBodyColor);
|
|
border: 1px solid transparent;
|
|
border-radius: 4px;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.charMemory_editorBulletInput:focus {
|
|
border-color: var(--SmartThemeBorderColor, rgba(255,255,255,0.2));
|
|
outline: none;
|
|
}
|
|
|
|
.charMemory_editorBullets {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.charMemory_editorAddBullet,
|
|
.charMemory_editorAddBlock {
|
|
font-size: 0.8em;
|
|
opacity: 0.7;
|
|
padding: 2px 8px;
|
|
}
|
|
|
|
.charMemory_editorAddBullet:hover,
|
|
.charMemory_editorAddBlock:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.charMemory_editorAddBlock {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.charMemory_editorCard {
|
|
position: relative;
|
|
}
|
|
```
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add index.js style.css
|
|
git commit -m "feat: replace textarea editor with card-based editor in consolidation dialog"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Rewrite consolidateMemories() for block-array data model
|
|
|
|
**Files:**
|
|
- Modify: `index.js:2552-2663` (rewrite `consolidateMemories`)
|
|
|
|
The function now works with block arrays instead of raw text. The version stack stores deep copies of block arrays. The editor is wired up via event delegation.
|
|
|
|
**Step 1: Replace `consolidateMemories()` entirely**
|
|
|
|
Replace lines 2552-2663 with:
|
|
|
|
```javascript
|
|
async function consolidateMemories() {
|
|
if (inApiCall) {
|
|
toastr.warning('An API call is already in progress.', 'CharMemory');
|
|
return;
|
|
}
|
|
|
|
const content = await readMemories();
|
|
const memories = parseMemories(content);
|
|
|
|
if (memories.length < 2) {
|
|
toastr.info('Not enough memories to consolidate.', 'CharMemory');
|
|
return;
|
|
}
|
|
|
|
const beforeCount = countMemories(memories);
|
|
logActivity(`Consolidation started: ${beforeCount} memories in ${memories.length} blocks`);
|
|
|
|
// Run initial consolidation — returns serialized text, parse to blocks
|
|
const initialResult = await runConsolidationLLM(memories);
|
|
if (!initialResult) return;
|
|
|
|
let editorBlocks = parseMemories(initialResult);
|
|
const versionStack = [];
|
|
|
|
// Helper: deep copy blocks array
|
|
const cloneBlocks = (blocks) => blocks.map(b => ({ ...b, bullets: [...b.bullets] }));
|
|
|
|
// Helper: re-render the editor pane from editorBlocks
|
|
const refreshEditor = () => {
|
|
const renderEditableCards = (blocks) => {
|
|
return blocks.map((b, bi) => {
|
|
const bullets = b.bullets.map((bullet, bui) =>
|
|
`<div class="charMemory_editorBulletRow" data-block="${bi}" data-bullet="${bui}">
|
|
<span class="charMemory_editorDash">-</span>
|
|
<input type="text" class="charMemory_editorBulletInput" value="${escapeHtml(bullet)}" data-block="${bi}" data-bullet="${bui}" />
|
|
<button class="charMemory_editorDeleteBullet menu_button menu_button_icon" data-block="${bi}" data-bullet="${bui}" title="Delete memory"><i class="fa-solid fa-trash fa-xs"></i></button>
|
|
</div>`
|
|
).join('');
|
|
return `<div class="charMemory_card charMemory_editorCard" data-block="${bi}">
|
|
<div class="charMemory_cardHeader">
|
|
<span class="charMemory_cardTitle">${escapeHtml(b.chat)}</span>
|
|
<span class="charMemory_cardTimestamp">${escapeHtml(b.date)}</span>
|
|
<span class="charMemory_cardActions">
|
|
<button class="charMemory_editorDeleteBlock menu_button menu_button_icon" data-block="${bi}" title="Delete block"><i class="fa-solid fa-trash"></i></button>
|
|
</span>
|
|
</div>
|
|
<div class="charMemory_editorBullets">${bullets}</div>
|
|
<button class="charMemory_editorAddBullet menu_button" data-block="${bi}"><i class="fa-solid fa-plus fa-xs"></i> Add memory</button>
|
|
</div>`;
|
|
}).join('');
|
|
};
|
|
$('#charMemory_editorPane').html(renderEditableCards(editorBlocks));
|
|
$('#charMemory_afterCount').text(countBlocksBullets(editorBlocks));
|
|
};
|
|
|
|
// Build and show the interactive dialog
|
|
const dialogHtml = buildConsolidationDialog(memories, beforeCount, editorBlocks);
|
|
const popup = callGenericPopup(dialogHtml, POPUP_TYPE.CONFIRM, '', { wide: true, allowVerticalScrolling: true });
|
|
|
|
// Set up the strategy dropdown to match current setting
|
|
const currentStrategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
|
$('#charMemory_consolidationDialogStrategy').val(currentStrategy);
|
|
|
|
// === Event delegation for editor interactions ===
|
|
|
|
// Sync bullet input changes back to editorBlocks
|
|
$(document).off('input.charMemoryEditor').on('input.charMemoryEditor', '.charMemory_editorBulletInput', function () {
|
|
const bi = Number($(this).data('block'));
|
|
const bui = Number($(this).data('bullet'));
|
|
if (editorBlocks[bi]) {
|
|
editorBlocks[bi].bullets[bui] = $(this).val();
|
|
}
|
|
});
|
|
|
|
// Delete bullet
|
|
$(document).off('click.charMemoryEditorDelBullet').on('click.charMemoryEditorDelBullet', '.charMemory_editorDeleteBullet', function () {
|
|
const bi = Number($(this).data('block'));
|
|
const bui = Number($(this).data('bullet'));
|
|
if (editorBlocks[bi]) {
|
|
editorBlocks[bi].bullets.splice(bui, 1);
|
|
// Remove block if no bullets left
|
|
if (editorBlocks[bi].bullets.length === 0) {
|
|
editorBlocks.splice(bi, 1);
|
|
}
|
|
refreshEditor();
|
|
}
|
|
});
|
|
|
|
// Delete block
|
|
$(document).off('click.charMemoryEditorDelBlock').on('click.charMemoryEditorDelBlock', '.charMemory_editorDeleteBlock', function () {
|
|
const bi = Number($(this).data('block'));
|
|
editorBlocks.splice(bi, 1);
|
|
refreshEditor();
|
|
});
|
|
|
|
// Add bullet to block
|
|
$(document).off('click.charMemoryEditorAddBullet').on('click.charMemoryEditorAddBullet', '.charMemory_editorAddBullet', function () {
|
|
const bi = Number($(this).data('block'));
|
|
if (editorBlocks[bi]) {
|
|
editorBlocks[bi].bullets.push('');
|
|
refreshEditor();
|
|
// Focus the new input
|
|
$(`#charMemory_editorPane .charMemory_editorCard[data-block="${bi}"] .charMemory_editorBulletInput:last`).focus();
|
|
}
|
|
});
|
|
|
|
// Add new block
|
|
$(document).off('click.charMemoryEditorAddBlock').on('click.charMemoryEditorAddBlock', '#charMemory_editorAddBlock', function () {
|
|
const now = new Date();
|
|
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
editorBlocks.push({ chat: 'consolidated', date: timestamp, bullets: [''] });
|
|
refreshEditor();
|
|
// Focus the new block's input
|
|
$('#charMemory_editorPane .charMemory_editorCard:last .charMemory_editorBulletInput:last').focus();
|
|
});
|
|
|
|
// === Re-run button ===
|
|
$('#charMemory_rerunConsolidation').off('click').on('click', async () => {
|
|
if (inApiCall) return;
|
|
|
|
const currentBlocks = cloneBlocks(editorBlocks);
|
|
|
|
const dialogStrategy = $('#charMemory_consolidationDialogStrategy').val();
|
|
extension_settings[MODULE_NAME].consolidationStrategy = dialogStrategy;
|
|
updateConsolidationStrategyUI();
|
|
saveSettingsDebounced();
|
|
|
|
$('#charMemory_rerunSpinner').show();
|
|
$('#charMemory_rerunConsolidation').prop('disabled', true);
|
|
|
|
const newResult = await runConsolidationLLM(memories);
|
|
|
|
$('#charMemory_rerunSpinner').hide();
|
|
$('#charMemory_rerunConsolidation').prop('disabled', false);
|
|
|
|
if (newResult) {
|
|
versionStack.push(currentBlocks);
|
|
$('#charMemory_undoRerun').prop('disabled', false);
|
|
editorBlocks = parseMemories(newResult);
|
|
refreshEditor();
|
|
}
|
|
});
|
|
|
|
// === Undo button ===
|
|
$('#charMemory_undoRerun').off('click').on('click', () => {
|
|
if (versionStack.length === 0) return;
|
|
editorBlocks = versionStack.pop();
|
|
refreshEditor();
|
|
if (versionStack.length === 0) {
|
|
$('#charMemory_undoRerun').prop('disabled', true);
|
|
}
|
|
});
|
|
|
|
// === Wait for Accept/Cancel ===
|
|
const confirmed = await popup;
|
|
|
|
// Clean up event delegation
|
|
$(document).off('input.charMemoryEditor');
|
|
$(document).off('click.charMemoryEditorDelBullet');
|
|
$(document).off('click.charMemoryEditorDelBlock');
|
|
$(document).off('click.charMemoryEditorAddBullet');
|
|
$(document).off('click.charMemoryEditorAddBlock');
|
|
|
|
if (!confirmed) {
|
|
logActivity('Consolidation cancelled by user');
|
|
toastr.info('Consolidation cancelled.', 'CharMemory');
|
|
return;
|
|
}
|
|
|
|
if (inApiCall) {
|
|
toastr.warning('Cannot save while a re-run is in progress.', 'CharMemory');
|
|
return;
|
|
}
|
|
|
|
// Filter out empty bullets and empty blocks before saving
|
|
const cleanBlocks = editorBlocks
|
|
.map(b => ({ ...b, bullets: b.bullets.filter(bullet => bullet.trim() !== '') }))
|
|
.filter(b => b.bullets.length > 0);
|
|
|
|
if (cleanBlocks.length === 0) {
|
|
toastr.warning('No memories to save. Memories unchanged.', 'CharMemory');
|
|
return;
|
|
}
|
|
|
|
consolidationBackup = content;
|
|
await writeMemories(serializeMemories(cleanBlocks));
|
|
$('#charMemory_undoConsolidate').prop('disabled', false);
|
|
|
|
const afterCount = countMemories(cleanBlocks);
|
|
logActivity(`Consolidation complete: ${beforeCount} → ${afterCount} memories`, 'success');
|
|
toastr.success(`Consolidated ${beforeCount} → ${afterCount} memories.`, 'CharMemory');
|
|
updateStatusDisplay();
|
|
}
|
|
```
|
|
|
|
Key changes from current:
|
|
- `editorBlocks` is an array of `{ chat, date, bullets }` objects instead of a text string
|
|
- `versionStack` stores deep copies of block arrays
|
|
- Event delegation handles bullet editing, deletion, addition, and block operations
|
|
- `refreshEditor()` re-renders the editor pane from `editorBlocks`
|
|
- On Accept, empty bullets/blocks are filtered before saving
|
|
- Event delegation is cleaned up when the popup closes
|
|
|
|
**Step 2: Commit**
|
|
|
|
```bash
|
|
git add index.js
|
|
git commit -m "feat: rewrite consolidateMemories() for card-based editor with block array data model"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Final integration, testing, and cleanup
|
|
|
|
**Files:**
|
|
- Modify: `index.js` (remove dead code, verify settings init)
|
|
- Modify: `style.css` (remove unused textarea styles)
|
|
- Modify: `CHANGELOG.md` (update for restructure)
|
|
|
|
**Step 1: Remove dead CSS**
|
|
|
|
Remove the `.charMemory_consolidationEditor` CSS block (the old textarea styles) if still present. It was defined around line 405 and should have been replaced in Task 3, but verify and remove any remnants.
|
|
|
|
**Step 2: Verify settings initialization**
|
|
|
|
In `loadSettings()`, verify that `updateConsolidationStrategyUI()` is still called and works (it was added in the original Task 2 and references `#charMemory_consolidationStrategy` which now lives in the Consolidate tab instead of Settings). The element IDs are unchanged, so no code changes should be needed.
|
|
|
|
**Step 3: Manual test checklist**
|
|
|
|
1. Open SillyTavern, switch to a character with existing memories
|
|
2. **Tab navigation**: Click each tab (Main, Consolidate, Batch Extract, Settings, Log) — verify correct content shows
|
|
3. **Main tab**: Extract Now and View/Edit work, Diagnostics refresh works
|
|
4. **Consolidate tab**: Strategy dropdown shows, custom prompt appears when "Custom" selected
|
|
5. **Consolidation dialog**:
|
|
- Click Consolidate → LLM runs → popup opens
|
|
- Left pane: read-only cards
|
|
- Right pane: editable cards with text inputs
|
|
- Edit a bullet → verify it persists in the card
|
|
- Delete a bullet (trash icon) → card updates, count updates
|
|
- Add a memory to a block → new input appears, focused
|
|
- Delete a block → block removed, count updates
|
|
- Add Block → new empty block appears at bottom
|
|
- Re-run → new cards appear, undo button enabled
|
|
- Undo → previous cards restored
|
|
- Accept → memories saved to file
|
|
6. **Undo Consolidation**: Restores pre-consolidation state
|
|
7. **Batch Extract tab**: Refresh loads chats, extraction works
|
|
8. **Settings tab**: All settings persist, provider config works
|
|
9. **Log tab**: Activity log shows entries for extraction and consolidation
|
|
|
|
**Step 4: Update CHANGELOG.md**
|
|
|
|
Add entries under `## 1.3.0`:
|
|
|
|
```markdown
|
|
### Improvements
|
|
|
|
- **Tabbed panel layout**: Extension panel reorganized into tabs (Main, Consolidate, Batch Extract, Settings, Log) for better discoverability.
|
|
- **Card-based consolidation editor**: Consolidated memories are now shown as editable cards matching the original memories' formatting, instead of raw text with tags. Add, edit, and delete individual memories or entire blocks.
|
|
```
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add index.js style.css CHANGELOG.md
|
|
git commit -m "feat: complete panel restructure with tabbed layout and card editor"
|
|
```
|
|
|
|
---
|
|
|
|
## Task Summary
|
|
|
|
| Task | Description | Files |
|
|
|------|-------------|-------|
|
|
| 1 | Restructure settings.html to top-level tab layout | `settings.html` |
|
|
| 2 | Update tab switching logic (verify, likely no changes) | `index.js` |
|
|
| 3 | Rewrite buildConsolidationDialog for card-based editor | `index.js`, `style.css` |
|
|
| 4 | Rewrite consolidateMemories() for block-array data model | `index.js` |
|
|
| 5 | Final integration, testing, cleanup, changelog | `index.js`, `style.css`, `CHANGELOG.md` |
|