mirror of
https://github.com/bal-spec/sillytavern-character-memory.git
synced 2026-04-28 03:39:44 +00:00
Merge branch 'consolidation-improvements' into beta
# Conflicts: # index.js # settings.html
This commit is contained in:
commit
bcae251cd4
14 changed files with 3529 additions and 337 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -1,5 +1,27 @@
|
|||
# Changelog
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **Consolidation strategy presets**: Choose between Conservative (only merge near-exact duplicates), Balanced (merge duplicates and related facts), or Aggressive (compress heavily, summarize themes). Each preset's prompt is viewable and editable.
|
||||
- **Card-based consolidation editor**: Consolidated memories are shown as editable cards matching the original memories' formatting, instead of raw text with tags. Add, edit, and delete individual memories or entire blocks directly in the preview.
|
||||
- **Re-run with version history**: Each re-run saves the previous version. Click Undo to step back through versions. The version stack lives within the dialog session.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Tabbed panel layout**: Extension panel reorganized into tabs (Main, Consolidate, Batch Extraction, Settings, Log) for better discoverability. Consolidation and batch extraction are now first-class tools with their own tabs.
|
||||
- **Consolidation config in context**: Strategy selection and custom prompt moved to the Consolidate tab, right where you use them, instead of buried in Settings.
|
||||
- **Read-only consolidation preview**: Consolidated memories now display as clean read-only cards by default, matching the original memories pane. Click the pencil icon on any block to enter edit mode for that block.
|
||||
- **Themed block headers**: The LLM now groups consolidated memories by theme (e.g., "Relationship History", "Key Events"). Theme names are editable.
|
||||
- **Editable strategy presets**: Each consolidation strategy (Conservative, Balanced, Aggressive) now has an expandable prompt viewer. Customize any preset's prompt and save it — with Restore Default to revert.
|
||||
- **Consolidation busy indicator**: The Consolidate button shows "Consolidating…" and disables during LLM processing.
|
||||
- **Persistent activity log**: A scrollable, resizable activity log is always visible at the bottom of the panel, regardless of which tab is active.
|
||||
- **Always-visible diagnostics**: Diagnostics moved from the Main tab to a permanent pane at the panel bottom with its own Refresh button.
|
||||
- **Cleaner panel layout**: Main tab shows "Memory Extraction" heading with automatic extraction toggle. Batch Extraction tab has "Character Attachments" header above the chat list.
|
||||
- **Optional chunk merging**: Multi-chunk extractions no longer merge into a single block by default. This keeps blocks smaller for long chats, making consolidation viable for memory-dense characters. Enable "Merge extraction chunks" in Settings to restore the old behavior.
|
||||
- **Date/time extraction**: The default extraction prompt now encourages capturing dates and times when mentioned in conversation, adding temporal context to memories.
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Bug Fixes
|
||||
|
|
|
|||
|
|
@ -346,6 +346,9 @@ Setting this too low (e.g., 10) gives the LLM too little context — it extracts
|
|||
**Max response length** (default: 1000 tokens, range: 100–4000)
|
||||
Token limit for the LLM's response per chunk. Most models produce well-formed output within 1000 tokens. **Reasoning/thinking models** (like GLM-4.7 on NVIDIA) need significantly more — their internal reasoning consumes tokens before producing the actual output. If you're using a thinking model and getting empty extractions, increase this to 2000–3000.
|
||||
|
||||
**Merge extraction chunks** (default: off)
|
||||
When a chat has more unprocessed messages than the chunk size, extraction runs in multiple passes. With this off (default), each chunk's memories are stored as separate blocks — keeping them small and manageable for consolidation. With this on, blocks from the same chat are merged into one. Disable this for long chats (hundreds of messages) where consolidation would be valuable — large merged blocks can exceed the consolidation LLM's capacity.
|
||||
|
||||
### How the Settings Interact
|
||||
|
||||
The three main sliders — **Extract after every N messages** (interval), **Minimum wait between extractions** (cooldown), and **Messages per LLM call** (chunk size) — work together:
|
||||
|
|
@ -452,7 +455,7 @@ Each extraction produces a `<memory>` block with chat attribution and timestampe
|
|||
**Key details:**
|
||||
- Each block is wrapped in `<memory>` tags with `chat` (the chat filename) and `date` (extraction timestamp) attributes
|
||||
- Bullets start with `- ` (dash space) — this is the only recognized format
|
||||
- Multiple blocks from the same chat are automatically merged by the extension
|
||||
- Multiple blocks from the same chat can optionally be merged (see "Merge extraction chunks" in Settings). This is off by default to keep blocks smaller for consolidation
|
||||
- The file is append-only during normal operation — new extractions add blocks at the end
|
||||
- Old files using the `## Memory N` heading format are auto-migrated on first read
|
||||
|
||||
|
|
@ -512,7 +515,7 @@ The extension listens for `CHARACTER_MESSAGE_RENDERED` events and counts charact
|
|||
5. If the LLM returns new `<memory>` blocks with bullets, appends them with chat ID and timestamp metadata
|
||||
6. If it returns `NO_NEW_MEMORIES`, skips the update
|
||||
7. Advances the extraction pointer and repeats for the next chunk until all unprocessed messages are covered
|
||||
8. Merges memory blocks from the same chat into a single block
|
||||
8. Optionally merges memory blocks from the same chat into a single block (off by default — enable "Merge extraction chunks" in Settings)
|
||||
9. Users can optionally consolidate memories manually using the Consolidate button (with preview and undo)
|
||||
|
||||
### Revectorization
|
||||
|
|
|
|||
64
docs/plans/2026-02-16-consolidation-ux-design.md
Normal file
64
docs/plans/2026-02-16-consolidation-ux-design.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Consolidation UX Improvement Design
|
||||
|
||||
**Date:** 2026-02-16
|
||||
**Status:** Approved
|
||||
|
||||
## Problem
|
||||
|
||||
Memory consolidation is currently all-or-nothing with no user control over strategy, no ability to edit results before saving, and no way to iterate on LLM output.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Strategy Presets + Custom Prompt
|
||||
|
||||
Add a **Consolidation Strategy** dropdown to the Settings drawer with 4 options:
|
||||
|
||||
- **Conservative** — only merge near-exact duplicates, preserve everything else
|
||||
- **Balanced** (default) — merge duplicates + combine related facts (refined version of current behavior)
|
||||
- **Aggressive** — compress heavily, summarize themes, minimize bullet count
|
||||
- **Custom** — reveals an editable textarea for a fully custom consolidation prompt
|
||||
|
||||
When a preset is selected, its prompt text is shown read-only. "Custom" makes the field editable. A "Restore Default" link resets to Balanced.
|
||||
|
||||
Persisted in `extension_settings.charMemory.consolidationStrategy` and `extension_settings.charMemory.consolidationPrompt`.
|
||||
|
||||
### 2. Iterative Preview Dialog
|
||||
|
||||
The consolidation preview becomes an interactive workspace:
|
||||
|
||||
**Layout:**
|
||||
- Left pane: "Original" — read-only display of current memories
|
||||
- Right pane: "Consolidated" — editable textarea showing the LLM's output
|
||||
- Top bar: Stats ("Original: 47 memories in 12 blocks -> Consolidated: 23 in 4 blocks")
|
||||
- Bottom toolbar: Re-run, Undo, Accept, Cancel buttons
|
||||
|
||||
**Version stack:** Each re-run pushes the current right-pane content onto an in-memory stack. Undo pops from the stack. No persistence needed.
|
||||
|
||||
**Flow:**
|
||||
1. User clicks Consolidate -> LLM runs -> preview opens with result (v1)
|
||||
2. User can edit bullets directly in the textarea
|
||||
3. User can change strategy -> click Re-run -> new result (v2), old saved to stack
|
||||
4. User can Undo to go back to previous version (including their edits)
|
||||
5. User clicks Accept -> parsed and saved to file
|
||||
|
||||
The textarea uses plain text in `<memory>` block format. On Accept, content is parsed through `parseMemories()` -> `serializeMemories()` to normalize.
|
||||
|
||||
### 3. Preset Prompt Text
|
||||
|
||||
**Conservative:**
|
||||
> Merge ONLY near-exact duplicate memories. If two bullets say essentially the same thing, keep the more detailed version. Do NOT combine loosely related facts. Do NOT summarize. Preserve every distinct piece of information.
|
||||
|
||||
**Balanced (default):**
|
||||
> Merge duplicate or near-duplicate memories into one. Combine closely related facts about the same event or topic. Preserve all unique information — do NOT discard distinct memories. Summarize in third person.
|
||||
|
||||
**Aggressive:**
|
||||
> Aggressively consolidate these memories into the fewest possible entries. Group by theme or topic. Summarize rather than listing individual events. It's OK to lose minor details if the key facts are preserved. Aim for a compact overview.
|
||||
|
||||
All presets share the structural rules: use `<memory>` tags, bullet format, no emojis, no commentary.
|
||||
|
||||
### 4. Unchanged
|
||||
|
||||
- Undo Consolidation button in main panel (session-only restore of pre-consolidation state)
|
||||
- `/consolidate-memories` slash command (opens the same dialog)
|
||||
- WebLLM truncation logic
|
||||
- Activity log entries for consolidation
|
||||
568
docs/plans/2026-02-16-consolidation-ux-plan.md
Normal file
568
docs/plans/2026-02-16-consolidation-ux-plan.md
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
# Consolidation UX Improvement Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Make memory consolidation interactive with strategy presets, editable preview, and re-run/undo within the dialog.
|
||||
|
||||
**Architecture:** Replaces the hardcoded `consolidationPrompt` constant with a preset system (`CONSOLIDATION_PRESETS`) and two new settings fields. Replaces the read-only `buildConsolidationPreview()` popup with an interactive dialog built as raw HTML (using `callGenericPopup` with `POPUP_TYPE.TEXT`). The dialog manages its own version stack and re-run logic internally.
|
||||
|
||||
**Tech Stack:** jQuery (ST convention), `callGenericPopup` for dialogs, `callLLM` for re-runs.
|
||||
|
||||
**Design doc:** `docs/plans/2026-02-16-consolidation-ux-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Consolidation Presets and Settings
|
||||
|
||||
**Files:**
|
||||
- Modify: `index.js:2378-2392` (replace `consolidationPrompt` constant)
|
||||
- Modify: `index.js:297-318` (add to `defaultSettings`)
|
||||
|
||||
**Step 1: Add the `CONSOLIDATION_PRESETS` object**
|
||||
|
||||
At `index.js` around line 2378, replace the `consolidationPrompt` constant with:
|
||||
|
||||
```javascript
|
||||
const CONSOLIDATION_PRESETS = {
|
||||
conservative: {
|
||||
name: 'Conservative',
|
||||
description: 'Only merge near-exact duplicates. Preserves everything else.',
|
||||
prompt: `Merge ONLY near-exact duplicate memories. If two bullets say essentially the same thing, keep the more detailed version. Do NOT combine loosely related facts. Do NOT summarize. Preserve every distinct piece of information.`,
|
||||
},
|
||||
balanced: {
|
||||
name: 'Balanced',
|
||||
description: 'Merge duplicates and combine related facts.',
|
||||
prompt: `Merge duplicate or near-duplicate memories into one. Combine closely related facts about the same event or topic. Preserve all unique information — do NOT discard distinct memories. Summarize in third person.`,
|
||||
},
|
||||
aggressive: {
|
||||
name: 'Aggressive',
|
||||
description: 'Compress heavily. Summarize themes. Minimize bullet count.',
|
||||
prompt: `Aggressively consolidate these memories into the fewest possible entries. Group by theme or topic. Summarize rather than listing individual events. It's OK to lose minor details if the key facts are preserved. Aim for a compact overview.`,
|
||||
},
|
||||
custom: {
|
||||
name: 'Custom',
|
||||
description: 'Write your own consolidation prompt.',
|
||||
prompt: '',
|
||||
},
|
||||
};
|
||||
|
||||
function buildConsolidationPrompt(memoriesText) {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
let userPrompt;
|
||||
if (strategy === 'custom') {
|
||||
userPrompt = extension_settings[MODULE_NAME].consolidationPrompt || CONSOLIDATION_PRESETS.balanced.prompt;
|
||||
} else {
|
||||
userPrompt = CONSOLIDATION_PRESETS[strategy]?.prompt || CONSOLIDATION_PRESETS.balanced.prompt;
|
||||
}
|
||||
return `You are a memory consolidation assistant. Review the following character memories and consolidate them.
|
||||
|
||||
RULES:
|
||||
${userPrompt}
|
||||
|
||||
ADDITIONAL FORMAT RULES:
|
||||
1. Do NOT use emojis anywhere in the output.
|
||||
2. Each consolidated memory must be wrapped in <memory></memory> tags.
|
||||
3. Inside each <memory> block, use a markdown bulleted list (lines starting with "- ").
|
||||
|
||||
MEMORIES TO CONSOLIDATE:
|
||||
${memoriesText}
|
||||
|
||||
Output ONLY <memory> blocks. No headers, no commentary, no extra text.`;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add new fields to `defaultSettings`**
|
||||
|
||||
In the `defaultSettings` object (~line 297), add two new fields:
|
||||
|
||||
```javascript
|
||||
consolidationStrategy: 'balanced',
|
||||
consolidationPrompt: '',
|
||||
```
|
||||
|
||||
Add these after the `extractionPrompt` field (line 302).
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add index.js
|
||||
git commit -m "feat: add consolidation strategy presets and buildConsolidationPrompt"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add Consolidation Settings UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `settings.html:194-203` (add consolidation section after extraction prompt section)
|
||||
- Modify: `index.js` (add UI initialization and event listeners)
|
||||
|
||||
**Step 1: Add HTML for consolidation strategy section**
|
||||
|
||||
In `settings.html`, after the extraction prompt section (after line 203, before the next `<hr>`), add:
|
||||
|
||||
```html
|
||||
<hr class="charMemory_separator" />
|
||||
|
||||
<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>
|
||||
```
|
||||
|
||||
**Step 2: Initialize UI values in `loadSettings()`**
|
||||
|
||||
In the `loadSettings()` function (around line 743 where other UI values are set), add:
|
||||
|
||||
```javascript
|
||||
$('#charMemory_consolidationStrategy').val(extension_settings[MODULE_NAME].consolidationStrategy || 'balanced');
|
||||
updateConsolidationStrategyUI();
|
||||
```
|
||||
|
||||
**Step 3: Add `updateConsolidationStrategyUI()` helper**
|
||||
|
||||
Add this helper function near the other UI helpers:
|
||||
|
||||
```javascript
|
||||
function updateConsolidationStrategyUI() {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
if (strategy === 'custom') {
|
||||
$('#charMemory_consolidationPrompt').show().val(extension_settings[MODULE_NAME].consolidationPrompt || '');
|
||||
$('#charMemory_consolidationPreview').hide();
|
||||
} else {
|
||||
$('#charMemory_consolidationPrompt').hide();
|
||||
const preset = CONSOLIDATION_PRESETS[strategy];
|
||||
$('#charMemory_consolidationPreview').show().text(preset ? preset.prompt : '');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Add event listeners in `setupListeners()`**
|
||||
|
||||
In `setupListeners()` (after the extraction prompt listeners around line 2718), add:
|
||||
|
||||
```javascript
|
||||
$('#charMemory_consolidationStrategy').off('change').on('change', function () {
|
||||
extension_settings[MODULE_NAME].consolidationStrategy = String($(this).val());
|
||||
updateConsolidationStrategyUI();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#charMemory_consolidationPrompt').off('input').on('input', function () {
|
||||
extension_settings[MODULE_NAME].consolidationPrompt = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add index.js settings.html
|
||||
git commit -m "feat: add consolidation strategy UI with presets and custom prompt"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Build the Interactive Consolidation Dialog
|
||||
|
||||
**Files:**
|
||||
- Modify: `index.js:2347-2362` (replace `buildConsolidationPreview`)
|
||||
- Modify: `style.css` (add styles for the new dialog)
|
||||
|
||||
**Step 1: Replace `buildConsolidationPreview` with interactive dialog builder**
|
||||
|
||||
Replace the `buildConsolidationPreview` function (lines 2347-2362) with:
|
||||
|
||||
```javascript
|
||||
function buildConsolidationDialog(beforeBlocks, beforeCount, consolidatedText) {
|
||||
const renderBefore = (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 afterCount = countConsolidatedText(consolidatedText);
|
||||
|
||||
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_consolidationPanes">
|
||||
<div class="charMemory_consolidationPane">
|
||||
<h4>Original</h4>
|
||||
<div class="charMemory_consolidationContent">${renderBefore(beforeBlocks)}</div>
|
||||
</div>
|
||||
<div class="charMemory_consolidationPane">
|
||||
<h4>Consolidated <small>(editable)</small></h4>
|
||||
<textarea id="charMemory_consolidationEditor" class="charMemory_consolidationEditor">${escapeHtml(consolidatedText)}</textarea>
|
||||
</div>
|
||||
</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>`;
|
||||
}
|
||||
|
||||
function countConsolidatedText(text) {
|
||||
const lines = text.split('\n').filter(l => l.trim().startsWith('- '));
|
||||
return lines.length;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add CSS for the consolidation dialog**
|
||||
|
||||
In `style.css`, add:
|
||||
|
||||
```css
|
||||
.charMemory_consolidationDialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.charMemory_consolidationStats {
|
||||
font-size: 0.9em;
|
||||
padding: 6px 10px;
|
||||
background: var(--SmartThemeBlurTintColor, rgba(0, 0, 0, 0.1));
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.charMemory_consolidationPanes {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.charMemory_consolidationPane {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.charMemory_consolidationPane h4 {
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.charMemory_consolidationContent {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.charMemory_consolidationEditor {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
resize: vertical;
|
||||
font-family: monospace;
|
||||
font-size: 0.85em;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--SmartThemeBlurTintColor, rgba(0, 0, 0, 0.05));
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor, rgba(255,255,255,0.1));
|
||||
}
|
||||
|
||||
.charMemory_consolidationToolbar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--SmartThemeBorderColor, rgba(255,255,255,0.1));
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add index.js style.css
|
||||
git commit -m "feat: add interactive consolidation dialog with editable textarea"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Rewrite `consolidateMemories()` to Use Interactive Dialog
|
||||
|
||||
**Files:**
|
||||
- Modify: `index.js:2394-2499` (rewrite `consolidateMemories()`)
|
||||
|
||||
**Step 1: Rewrite `consolidateMemories()`**
|
||||
|
||||
Replace the entire function 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
|
||||
const initialResult = await runConsolidationLLM(memories);
|
||||
if (!initialResult) return;
|
||||
|
||||
// Build and show the interactive dialog
|
||||
const dialogHtml = buildConsolidationDialog(memories, beforeCount, initialResult);
|
||||
const versionStack = [];
|
||||
|
||||
// Use TEXT popup so we control accept/cancel via our own logic
|
||||
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);
|
||||
|
||||
// Wire up re-run button
|
||||
$('#charMemory_rerunConsolidation').off('click').on('click', async () => {
|
||||
if (inApiCall) return;
|
||||
|
||||
// Push current editor content to version stack
|
||||
const currentText = $('#charMemory_consolidationEditor').val();
|
||||
versionStack.push(currentText);
|
||||
$('#charMemory_undoRerun').prop('disabled', false);
|
||||
|
||||
// Update strategy from dialog dropdown
|
||||
const dialogStrategy = $('#charMemory_consolidationDialogStrategy').val();
|
||||
extension_settings[MODULE_NAME].consolidationStrategy = dialogStrategy;
|
||||
updateConsolidationStrategyUI();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Run LLM
|
||||
$('#charMemory_rerunSpinner').show();
|
||||
$('#charMemory_rerunConsolidation').prop('disabled', true);
|
||||
|
||||
const newResult = await runConsolidationLLM(memories);
|
||||
|
||||
$('#charMemory_rerunSpinner').hide();
|
||||
$('#charMemory_rerunConsolidation').prop('disabled', false);
|
||||
|
||||
if (newResult) {
|
||||
$('#charMemory_consolidationEditor').val(newResult);
|
||||
$('#charMemory_afterCount').text(countConsolidatedText(newResult));
|
||||
}
|
||||
});
|
||||
|
||||
// Wire up undo button
|
||||
$('#charMemory_undoRerun').off('click').on('click', () => {
|
||||
if (versionStack.length === 0) return;
|
||||
const previousText = versionStack.pop();
|
||||
$('#charMemory_consolidationEditor').val(previousText);
|
||||
$('#charMemory_afterCount').text(countConsolidatedText(previousText));
|
||||
if (versionStack.length === 0) {
|
||||
$('#charMemory_undoRerun').prop('disabled', true);
|
||||
}
|
||||
});
|
||||
|
||||
// Update count on editor change
|
||||
$('#charMemory_consolidationEditor').off('input').on('input', function () {
|
||||
$('#charMemory_afterCount').text(countConsolidatedText($(this).val()));
|
||||
});
|
||||
|
||||
// Wait for user to accept or cancel
|
||||
const confirmed = await popup;
|
||||
if (!confirmed) {
|
||||
logActivity('Consolidation cancelled by user');
|
||||
toastr.info('Consolidation cancelled.', 'CharMemory');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the editor content and save
|
||||
const editedText = $('#charMemory_consolidationEditor').val();
|
||||
const parsed = parseMemories(editedText);
|
||||
if (parsed.length === 0) {
|
||||
toastr.warning('Could not parse any memories from the edited text. Memories unchanged.', 'CharMemory');
|
||||
return;
|
||||
}
|
||||
|
||||
consolidationBackup = content;
|
||||
await writeMemories(serializeMemories(parsed));
|
||||
$('#charMemory_undoConsolidate').prop('disabled', false);
|
||||
|
||||
const afterCount = countMemories(parsed);
|
||||
logActivity(`Consolidation complete: ${beforeCount} → ${afterCount} memories`, 'success');
|
||||
toastr.success(`Consolidated ${beforeCount} → ${afterCount} memories.`, 'CharMemory');
|
||||
updateStatusDisplay();
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Extract `runConsolidationLLM()` helper**
|
||||
|
||||
Add this function before `consolidateMemories()`:
|
||||
|
||||
```javascript
|
||||
async function runConsolidationLLM(memories) {
|
||||
let memoriesText = memories.map((b, i) =>
|
||||
`[Block ${i + 1}]\n${b.bullets.map(bullet => `- ${bullet}`).join('\n')}`,
|
||||
).join('\n\n');
|
||||
|
||||
const isWebLlm = extension_settings[MODULE_NAME].source === EXTRACTION_SOURCE.WEBLLM;
|
||||
if (isWebLlm) {
|
||||
const template = buildConsolidationPrompt('');
|
||||
const available = Math.max(WEBLLM_MAX_PROMPT_CHARS - template.length, 1000);
|
||||
memoriesText = truncateText(memoriesText, available);
|
||||
}
|
||||
|
||||
let prompt = buildConsolidationPrompt(memoriesText);
|
||||
prompt = substituteParamsExtended(prompt);
|
||||
|
||||
try {
|
||||
inApiCall = true;
|
||||
const sourceLabel = getSourceLabel();
|
||||
toastr.info(`Consolidating via ${sourceLabel}...`, 'CharMemory', { timeOut: 3000 });
|
||||
|
||||
const verbose = extension_settings[MODULE_NAME].verboseLogging;
|
||||
if (verbose) {
|
||||
logActivity(`Consolidation prompt sent to ${sourceLabel} (${prompt.length} chars):\n${prompt}`);
|
||||
}
|
||||
|
||||
logActivity(`Sending consolidation to ${sourceLabel}... waiting for response`);
|
||||
const llmStartTime = Date.now();
|
||||
const result = await callLLM(
|
||||
prompt,
|
||||
extension_settings[MODULE_NAME].responseLength * 2,
|
||||
'You are a memory consolidation assistant.',
|
||||
);
|
||||
|
||||
const llmElapsed = ((Date.now() - llmStartTime) / 1000).toFixed(1);
|
||||
logActivity(`Consolidation response received from ${sourceLabel} in ${llmElapsed}s (${(result || '').length} chars)`);
|
||||
if (verbose && result) {
|
||||
logActivity(`Raw consolidation response:\n${result}`);
|
||||
}
|
||||
|
||||
let cleanResult = removeReasoningFromString(result);
|
||||
cleanResult = cleanResult.trim();
|
||||
|
||||
if (!cleanResult) {
|
||||
logActivity('Consolidation returned empty result', 'warning');
|
||||
toastr.warning('Consolidation returned empty result.', 'CharMemory');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse into memory format, then serialize back to plain text for the editor
|
||||
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')}`;
|
||||
|
||||
const consolidationRegex = /<memory>([\s\S]*?)<\/memory>/gi;
|
||||
const consolidationMatches = [...cleanResult.matchAll(consolidationRegex)];
|
||||
const rawEntries = consolidationMatches.length > 0
|
||||
? consolidationMatches.map(m => m[1].trim()).filter(Boolean)
|
||||
: [cleanResult.trim()].filter(Boolean);
|
||||
|
||||
const consolidated = rawEntries.map(entry => {
|
||||
const bullets = entry.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.startsWith('- '))
|
||||
.map(l => l.slice(2).trim())
|
||||
.filter(Boolean);
|
||||
return { chat: 'consolidated', date: timestamp, bullets: bullets.length > 0 ? bullets : [entry] };
|
||||
});
|
||||
|
||||
return serializeMemories(consolidated);
|
||||
} catch (err) {
|
||||
console.error(LOG_PREFIX, 'Consolidation failed:', err);
|
||||
logActivity(`Consolidation failed: ${err.message}`, 'error');
|
||||
toastr.error('Memory consolidation failed. Check console for details.', 'CharMemory');
|
||||
return null;
|
||||
} finally {
|
||||
inApiCall = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add index.js
|
||||
git commit -m "feat: rewrite consolidateMemories with interactive dialog, re-run, and undo"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Final Integration and Manual Testing
|
||||
|
||||
**Files:**
|
||||
- Modify: `index.js` (verify loadSettings initialization)
|
||||
- Modify: `CHANGELOG.md` (add entry)
|
||||
|
||||
**Step 1: Verify settings migration**
|
||||
|
||||
Check that existing users who have no `consolidationStrategy` setting won't break. In `loadSettings()`, ensure the default fallback works:
|
||||
|
||||
```javascript
|
||||
// After Object.assign for settings, no explicit migration needed —
|
||||
// defaultSettings provides 'balanced' and buildConsolidationPrompt()
|
||||
// falls back to 'balanced' if the field is missing.
|
||||
```
|
||||
|
||||
**Step 2: Manual test checklist**
|
||||
|
||||
1. Open SillyTavern, go to a character with existing memories
|
||||
2. Open CharMemory panel → Settings → verify "Consolidation strategy" dropdown appears with 4 options
|
||||
3. Select each preset → verify the description text shows below the dropdown
|
||||
4. Select "Custom" → verify the textarea appears
|
||||
5. Click "Consolidate" → verify the interactive dialog opens with:
|
||||
- Left pane showing original memories
|
||||
- Right pane showing editable textarea with consolidated result
|
||||
- Stats bar at top
|
||||
- Strategy dropdown, Re-run, Undo buttons at bottom
|
||||
6. Edit text in the textarea → verify the count updates live
|
||||
7. Change strategy in dialog → click Re-run → verify new result appears
|
||||
8. Click Undo → verify previous version is restored
|
||||
9. Click Undo again → verify button disables when stack is empty
|
||||
10. Click Accept → verify memories are saved
|
||||
11. Click "Undo Consolidation" → verify original memories restore
|
||||
12. Test cancel: run consolidation, click Cancel → verify memories unchanged
|
||||
|
||||
**Step 3: Update CHANGELOG.md**
|
||||
|
||||
Add entry for the new version.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add index.js CHANGELOG.md
|
||||
git commit -m "feat: complete consolidation UX improvements with strategy presets"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task Summary
|
||||
|
||||
| Task | Description | Files |
|
||||
|------|-------------|-------|
|
||||
| 1 | Add consolidation presets + `buildConsolidationPrompt()` | `index.js` |
|
||||
| 2 | Add consolidation strategy UI (HTML + listeners) | `settings.html`, `index.js` |
|
||||
| 3 | Build interactive dialog HTML/CSS | `index.js`, `style.css` |
|
||||
| 4 | Rewrite `consolidateMemories()` with dialog + re-run + undo | `index.js` |
|
||||
| 5 | Integration, testing, changelog | `index.js`, `CHANGELOG.md` |
|
||||
98
docs/plans/2026-02-16-panel-restructure-design.md
Normal file
98
docs/plans/2026-02-16-panel-restructure-design.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Panel Restructure + Card-Based Consolidation Editor Design
|
||||
|
||||
**Date:** 2026-02-16
|
||||
**Status:** Approved
|
||||
**Branch:** consolidation-improvements (builds on existing consolidation UX work)
|
||||
|
||||
## Problem
|
||||
|
||||
1. Consolidation and batch extraction are buried as a button and a sub-tab. They should be first-class tools.
|
||||
2. Consolidation config (strategy, prompt) is buried in Settings, separate from where consolidation happens.
|
||||
3. The consolidation preview shows nicely formatted cards on the left but raw `<memory>` tags on the right, which is jarring.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Top-Level Tab Layout
|
||||
|
||||
The extension panel becomes tab-based. Stats bar and auto-extraction toggle stay above.
|
||||
|
||||
```
|
||||
Character Memory
|
||||
Stats bar (file, count, progress, cooldown)
|
||||
Enable auto-extraction checkbox
|
||||
|
||||
[Main] [Consolidate] [Batch Extract] [Settings] [Log]
|
||||
|
||||
Main tab (default):
|
||||
[Extract Now] [View / Edit]
|
||||
Diagnostics section (collapsed)
|
||||
|
||||
Consolidate tab:
|
||||
Strategy dropdown (conservative/balanced/aggressive/custom)
|
||||
Custom prompt textarea (when custom) or preset description
|
||||
[Consolidate] [Undo Consolidation]
|
||||
|
||||
Batch Extract tab:
|
||||
Same content as current batch extract tab
|
||||
|
||||
Settings tab:
|
||||
LLM/Provider config
|
||||
Auto-Extraction sliders
|
||||
Extraction Settings (chunk size, response length)
|
||||
Storage (per-chat, filename)
|
||||
Extraction prompt
|
||||
Reset/Clear buttons
|
||||
|
||||
Log tab:
|
||||
Activity Log with verbose toggle, clear, save
|
||||
```
|
||||
|
||||
### 2. Card-Based Consolidation Editor (Popup)
|
||||
|
||||
Clicking "Consolidate" runs the LLM and opens a full-width popup:
|
||||
|
||||
**Layout:**
|
||||
- Top: Stats bar ("Original: 47 memories -> Consolidated: 23")
|
||||
- Below stats: Strategy dropdown + Re-run button + Undo button + spinner
|
||||
- Main area: Two-pane side-by-side
|
||||
- Left: Original memories as read-only cards (same as current)
|
||||
- Right: Consolidated memories as editable cards
|
||||
- Bottom: Accept / Cancel buttons (from CONFIRM popup type)
|
||||
|
||||
**Editable card structure:**
|
||||
- Each block is a card matching the left pane's visual style
|
||||
- Each bullet is an always-editable text input with a trash icon button
|
||||
- "Add memory" button at the bottom of each card to add a new bullet
|
||||
- "Delete block" button to remove the entire card
|
||||
- "Add Block" button at the very bottom to create a new empty block
|
||||
|
||||
**Data model:**
|
||||
- Editor operates on an in-memory array of block objects: `[{ chat, date, bullets }]`
|
||||
- Same structure as `parseMemories()` output
|
||||
- Add/delete/edit mutate this array directly (via data attributes on DOM elements)
|
||||
- On Accept: `serializeMemories(blocks)` to save
|
||||
- Live bullet count updates as blocks are modified
|
||||
|
||||
**Version stack:**
|
||||
- Re-run pushes deep copy of current blocks array
|
||||
- Undo pops from stack
|
||||
- Same behavior as current, but operating on block arrays instead of text
|
||||
|
||||
### 3. What Changes From Current Implementation
|
||||
|
||||
- `settings.html`: Complete restructure to tab layout
|
||||
- `buildConsolidationDialog()`: Rewrite right pane from textarea to card-based editor
|
||||
- `consolidateMemories()`: Work with block arrays instead of raw text for the editor
|
||||
- `countConsolidatedText()`: Replace with counting from the block array
|
||||
- CSS: New tab styles, editor input styles
|
||||
- Consolidation strategy UI moves from Settings drawer to Consolidate tab
|
||||
|
||||
### 4. What Stays The Same
|
||||
|
||||
- `runConsolidationLLM()` helper (returns serialized text, parsed by caller)
|
||||
- `CONSOLIDATION_PRESETS` and `buildConsolidationPrompt()`
|
||||
- `consolidationBackup` / Undo Consolidation mechanism
|
||||
- Settings persistence
|
||||
- Slash commands
|
||||
- Memory manager popup (View/Edit) — unchanged
|
||||
- All extraction logic — unchanged
|
||||
829
docs/plans/2026-02-16-panel-restructure-plan.md
Normal file
829
docs/plans/2026-02-16-panel-restructure-plan.md
Normal file
|
|
@ -0,0 +1,829 @@
|
|||
# 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` |
|
||||
93
docs/plans/2026-02-17-consolidation-ux-refinements-design.md
Normal file
93
docs/plans/2026-02-17-consolidation-ux-refinements-design.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Consolidation Dialog UX Refinements — Design
|
||||
|
||||
**Date:** 2026-02-17
|
||||
**Branch:** `consolidation-improvements`
|
||||
**Status:** Approved
|
||||
|
||||
## Problem
|
||||
|
||||
After user testing the card-based editor and tabbed panel layout, several UX issues surfaced:
|
||||
|
||||
1. Left pane bullets are indented oddly (fixed — CSS committed)
|
||||
2. Pane headings "Original" / "Consolidated" are too terse
|
||||
3. Right pane is always editable — jarring compared to left pane's clean read-only cards
|
||||
4. Block headers show "consolidated 2026-02-17 21:22" which is meaningless to users
|
||||
5. Add Memory / Add Block buttons are confusing when always visible
|
||||
6. Users can't see the actual consolidation prompt — only a preset name
|
||||
7. Activity log is hidden behind the Log tab; users want persistent status visibility
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Pane Headings
|
||||
|
||||
- "Original" → **"Original Memories"**
|
||||
- "Consolidated" → **"Consolidated Memories"**
|
||||
|
||||
### 2. Read-Only Default with Per-Block Edit
|
||||
|
||||
Both panes render identically by default — read-only cards with bullet lists.
|
||||
|
||||
Each block in the right pane gets a pencil icon in its header. Clicking it switches **just that block** to edit mode:
|
||||
- Bullets become text inputs with delete buttons
|
||||
- "Add Memory" button appears at the bottom of the block
|
||||
- The pencil icon becomes a checkmark; clicking it exits edit mode
|
||||
|
||||
The "Add Block" button at the bottom of the right pane only appears when any block is in edit mode.
|
||||
|
||||
### 3. Themed Numbered Block Headers
|
||||
|
||||
The consolidation prompt is updated to instruct the LLM: "Group memories by theme and name each group."
|
||||
|
||||
The LLM returns blocks with theme names. Headers display as: **"1. Relationship History"**, **"2. Character Background"**, etc.
|
||||
|
||||
In edit mode, the theme name is an editable text input.
|
||||
|
||||
**Data format:** The `chat` field in `<memory chat="..." date="...">` carries the theme name. Example: `<memory chat="Relationship History" date="2026-02-17 21:22">`. Original memories keep their existing chat/date headers unchanged.
|
||||
|
||||
### 4. Add Memory / Add Block — Edit Mode Only
|
||||
|
||||
- "Add Memory" button only visible inside blocks that are in edit mode
|
||||
- "Add Block" button only visible when at least one block is in edit mode
|
||||
|
||||
### 5. Editable Presets with Expandable Prompt Viewer
|
||||
|
||||
Remove the "Custom" preset. Keep 3 presets: **Conservative**, **Balanced**, **Aggressive**.
|
||||
|
||||
Each preset has a collapsible "Show prompt" disclosure below the dropdown. Expanding it reveals the full prompt text in an editable textarea. Changes are saved per-preset. A "Restore Default" button resets to built-in text.
|
||||
|
||||
Same expandable prompt viewer appears in both:
|
||||
- The **Consolidate tab** in the panel
|
||||
- The **consolidation dialog** toolbar
|
||||
|
||||
**Storage:** `extension_settings.charMemory.consolidationPrompts.{conservative,balanced,aggressive}` — if present, overrides the built-in default for that preset. If absent or empty, the built-in default is used.
|
||||
|
||||
### 6. Persistent Activity Log at Panel Bottom
|
||||
|
||||
A compact log section at the bottom of the panel, **always visible regardless of active tab**.
|
||||
|
||||
- Shows 2-3 lines of recent activity by default
|
||||
- Clickable/expandable — expands upward to show more history
|
||||
- Uses the same `logActivity()` entries that go to the Log tab
|
||||
- The full Log tab remains for verbose/complete history and log management (clear, save, verbose toggle)
|
||||
|
||||
## What Stays The Same
|
||||
|
||||
- `runConsolidationLLM()` — no changes to the LLM call mechanics
|
||||
- `parseMemories()` / `serializeMemories()` — the `<memory>` tag format is unchanged, just the `chat` field carries theme names for consolidated blocks
|
||||
- Tab layout (Main, Consolidate, Batch Extract, Settings, Log) — unchanged
|
||||
- Stats bar, enable checkbox — unchanged
|
||||
- Re-run / Undo version stack — unchanged (stores block arrays)
|
||||
- Memory manager popup — unchanged
|
||||
- Slash commands — unchanged
|
||||
|
||||
## What Changes
|
||||
|
||||
| Component | Before | After |
|
||||
|-----------|--------|-------|
|
||||
| Right pane default state | Always-editable inputs | Read-only cards, per-block edit toggle |
|
||||
| Block headers (consolidated) | "consolidated 2026-02-17 21:22" | "1. Relationship History" (themed, numbered) |
|
||||
| Add Memory / Add Block | Always visible | Only in edit mode |
|
||||
| Preset system | 3 presets + Custom | 3 presets, each editable with expandable prompt viewer |
|
||||
| Prompt visibility | Name only (dropdown) | Expandable disclosure shows full prompt text |
|
||||
| Activity log | Log tab only | Persistent compact log at panel bottom + full Log tab |
|
||||
| Pane headings | "Original" / "Consolidated" | "Original Memories" / "Consolidated Memories" |
|
||||
689
docs/plans/2026-02-17-ux-refinements-plan.md
Normal file
689
docs/plans/2026-02-17-ux-refinements-plan.md
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
# Consolidation UX Refinements Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Refine the consolidation dialog UX based on user testing feedback — read-only default with per-block edit, themed headers, editable presets with prompt viewer, and persistent activity log.
|
||||
|
||||
**Architecture:** The consolidation dialog gets a dual-mode card renderer (read-only/edit per block). The preset system changes from 3+Custom to 3 editable presets with expandable prompt disclosure. A persistent mini-log is added below the tab content area. All changes are in `index.js`, `settings.html`, and `style.css`.
|
||||
|
||||
**Tech Stack:** jQuery (ST convention), `callGenericPopup` for dialogs, existing `charMemory_card` CSS system.
|
||||
|
||||
**Design doc:** `docs/plans/2026-02-17-consolidation-ux-refinements-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Editable presets — remove Custom, add per-preset prompt storage
|
||||
|
||||
**Files:**
|
||||
- Modify: `index.js:2448-2468` (CONSOLIDATION_PRESETS — remove `custom` entry)
|
||||
- Modify: `index.js:2471-2494` (buildConsolidationPrompt — use saved prompt override)
|
||||
- Modify: `index.js:297-304` (defaultSettings — replace `consolidationPrompt` with `consolidationPrompts`)
|
||||
- Modify: `index.js:504-514` (updateConsolidationStrategyUI — rewrite for expandable prompt)
|
||||
|
||||
**Step 1: Update CONSOLIDATION_PRESETS**
|
||||
|
||||
Remove the `custom` entry. Keep only `conservative`, `balanced`, `aggressive`:
|
||||
|
||||
```javascript
|
||||
const CONSOLIDATION_PRESETS = {
|
||||
conservative: {
|
||||
name: 'Conservative',
|
||||
description: 'Only merge near-exact duplicates. Preserves everything else.',
|
||||
prompt: `Merge ONLY near-exact duplicate memories. If two bullets say essentially the same thing, keep the more detailed version. Do NOT combine loosely related facts. Do NOT summarize. Preserve every distinct piece of information.`,
|
||||
},
|
||||
balanced: {
|
||||
name: 'Balanced',
|
||||
description: 'Merge duplicates and combine related facts.',
|
||||
prompt: `Merge duplicate or near-duplicate memories into one. Combine closely related facts about the same event or topic. Preserve all unique information — do NOT discard distinct memories. Summarize in third person.`,
|
||||
},
|
||||
aggressive: {
|
||||
name: 'Aggressive',
|
||||
description: 'Compress heavily. Summarize themes. Minimize bullet count.',
|
||||
prompt: `Aggressively consolidate these memories into the fewest possible entries. Group by theme or topic. Summarize rather than listing individual events. It's OK to lose minor details if the key facts are preserved. Aim for a compact overview.`,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Step 2: Update defaultSettings**
|
||||
|
||||
Replace `consolidationPrompt: ''` with `consolidationPrompts: {}`:
|
||||
|
||||
```javascript
|
||||
const defaultSettings = {
|
||||
// ...existing fields...
|
||||
consolidationStrategy: 'balanced',
|
||||
consolidationPrompts: {}, // per-preset overrides: { conservative: '...', balanced: '...', aggressive: '...' }
|
||||
// ...rest...
|
||||
};
|
||||
```
|
||||
|
||||
**Step 3: Update buildConsolidationPrompt**
|
||||
|
||||
Change the prompt lookup to check for user overrides:
|
||||
|
||||
```javascript
|
||||
function buildConsolidationPrompt(memoriesText) {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
const overrides = extension_settings[MODULE_NAME].consolidationPrompts || {};
|
||||
const userPrompt = overrides[strategy]
|
||||
|| CONSOLIDATION_PRESETS[strategy]?.prompt
|
||||
|| CONSOLIDATION_PRESETS.balanced.prompt;
|
||||
// ...rest of function unchanged...
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Update updateConsolidationStrategyUI**
|
||||
|
||||
Rewrite to handle the expandable prompt viewer (replaces old custom textarea / preview logic):
|
||||
|
||||
```javascript
|
||||
function updateConsolidationStrategyUI() {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
const overrides = extension_settings[MODULE_NAME].consolidationPrompts || {};
|
||||
const currentPrompt = overrides[strategy] || CONSOLIDATION_PRESETS[strategy]?.prompt || '';
|
||||
const isCustomized = !!overrides[strategy];
|
||||
|
||||
// Update the prompt textarea value
|
||||
$('#charMemory_consolidationPrompt').val(currentPrompt);
|
||||
|
||||
// Show/hide the restore default button
|
||||
$('#charMemory_restorePresetDefault').toggle(isCustomized);
|
||||
|
||||
// Update preview text (shown when collapsed)
|
||||
const previewText = isCustomized ? `${CONSOLIDATION_PRESETS[strategy]?.name} (customized)` : CONSOLIDATION_PRESETS[strategy]?.description || '';
|
||||
$('#charMemory_consolidationPreview').text(previewText);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add index.js
|
||||
git commit -m "refactor: replace Custom preset with per-preset editable prompts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Consolidate tab HTML — expandable prompt viewer
|
||||
|
||||
**Files:**
|
||||
- Modify: `settings.html:60-79` (Consolidate tab content)
|
||||
|
||||
**Step 1: Rewrite Consolidate tab HTML**
|
||||
|
||||
Replace lines 60-79 with:
|
||||
|
||||
```html
|
||||
<!-- 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>
|
||||
</select>
|
||||
<small id="charMemory_consolidationPreview" class="charMemory_helperText" style="font-style:italic;"></small>
|
||||
<details class="charMemory_promptDisclosure" id="charMemory_promptDisclosure">
|
||||
<summary><small>Show prompt</small></summary>
|
||||
<textarea id="charMemory_consolidationPrompt" class="text_pole textarea_compact" rows="6" placeholder="Edit the consolidation prompt for this strategy..."></textarea>
|
||||
<div class="charMemory_buttonRow">
|
||||
<input type="button" id="charMemory_restorePresetDefault" class="menu_button" value="Restore Default" title="Reset this preset's prompt to its built-in default" style="display:none;" />
|
||||
</div>
|
||||
</details>
|
||||
</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>
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- Removed `custom` option from dropdown
|
||||
- Replaced hidden textarea + preview with `<details>` disclosure containing the textarea
|
||||
- Added "Restore Default" button inside the disclosure
|
||||
- Preview text (collapsed summary) stays as a `<small>` above the disclosure
|
||||
|
||||
**Step 2: Add event listeners for prompt editing and restore**
|
||||
|
||||
In `setupListeners()`, add handlers for the prompt textarea and restore button:
|
||||
|
||||
```javascript
|
||||
// Consolidation prompt editing — save override when user edits
|
||||
$('#charMemory_consolidationPrompt').off('input').on('input', function () {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
if (!extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
extension_settings[MODULE_NAME].consolidationPrompts = {};
|
||||
}
|
||||
extension_settings[MODULE_NAME].consolidationPrompts[strategy] = $(this).val();
|
||||
$('#charMemory_restorePresetDefault').show();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
// Restore preset default
|
||||
$('#charMemory_restorePresetDefault').off('click').on('click', function () {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
if (extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
delete extension_settings[MODULE_NAME].consolidationPrompts[strategy];
|
||||
}
|
||||
updateConsolidationStrategyUI();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
```
|
||||
|
||||
Also update the strategy dropdown handler to call `updateConsolidationStrategyUI()` (it likely already does — verify and ensure it doesn't reference the old `custom` logic).
|
||||
|
||||
**Step 3: Add CSS for the disclosure**
|
||||
|
||||
```css
|
||||
.charMemory_promptDisclosure {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.charMemory_promptDisclosure summary {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.charMemory_promptDisclosure summary:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.charMemory_promptDisclosure textarea {
|
||||
margin-top: 4px;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add settings.html index.js style.css
|
||||
git commit -m "feat: add expandable prompt viewer with per-preset editing"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Themed block headers — update prompt and parsing
|
||||
|
||||
**Files:**
|
||||
- Modify: `index.js:2479-2494` (buildConsolidationPrompt — add theme instruction)
|
||||
- Modify: `index.js:2540-2564` (runConsolidationLLM — parse chat field from LLM output)
|
||||
|
||||
**Step 1: Update consolidation prompt format rules**
|
||||
|
||||
In `buildConsolidationPrompt`, update the ADDITIONAL FORMAT RULES to instruct the LLM to use themed blocks:
|
||||
|
||||
```javascript
|
||||
return `You are a memory consolidation assistant. Review the following character memories and consolidate them.
|
||||
|
||||
RULES:
|
||||
${userPrompt}
|
||||
|
||||
ADDITIONAL FORMAT RULES:
|
||||
1. Do NOT use emojis anywhere in the output.
|
||||
2. Do NOT copy text verbatim from the input — rephrase in third person.
|
||||
3. Group memories by theme. Each group is wrapped in <memory chat="Theme Name"></memory> tags where "Theme Name" is a short descriptive label (e.g. "Relationship History", "Character Background", "Key Events").
|
||||
4. Inside each <memory> block, use a markdown bulleted list (lines starting with "- ").
|
||||
|
||||
MEMORIES TO CONSOLIDATE:
|
||||
${memoriesText}
|
||||
|
||||
Output ONLY <memory> blocks. No headers, no commentary, no extra text.`;
|
||||
```
|
||||
|
||||
**Step 2: Update runConsolidationLLM parsing to extract chat attribute**
|
||||
|
||||
Currently (line 2549), the parsing uses a simple `<memory>` regex without attributes. Update it to extract the `chat` attribute:
|
||||
|
||||
```javascript
|
||||
const consolidationRegex = /<memory(?:\s+chat="([^"]*)")?>([\s\S]*?)<\/memory>/gi;
|
||||
const consolidationMatches = [...cleanResult.matchAll(consolidationRegex)];
|
||||
const rawEntries = consolidationMatches.length > 0
|
||||
? consolidationMatches.map(m => ({ theme: m[1] || 'Consolidated', content: m[2].trim() })).filter(e => e.content)
|
||||
: [{ theme: 'Consolidated', content: cleanResult.trim() }].filter(e => e.content);
|
||||
|
||||
const consolidated = rawEntries.map((entry, i) => {
|
||||
const bullets = entry.content.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.startsWith('- '))
|
||||
.map(l => l.slice(2).trim())
|
||||
.filter(Boolean);
|
||||
return { chat: entry.theme, date: timestamp, bullets: bullets.length > 0 ? bullets : [entry.content] };
|
||||
});
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add index.js
|
||||
git commit -m "feat: add themed block headers to consolidation output"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Read-only default with per-block edit toggle
|
||||
|
||||
**Files:**
|
||||
- Modify: `index.js:2369-2390` (renderEditableCards → replace with renderConsolidatedCards dual-mode)
|
||||
- Modify: `index.js:2392-2432` (buildConsolidationDialog — use new renderer, update headings)
|
||||
- Modify: `index.js:2575-2760` (consolidateMemories — rewrite event delegation for edit mode)
|
||||
- Modify: `style.css` (add edit mode toggle styles)
|
||||
|
||||
This is the largest task. The right pane cards need two modes: read-only (default) and edit (per-block toggle).
|
||||
|
||||
**Step 1: Replace renderEditableCards with renderConsolidatedCards**
|
||||
|
||||
This new function renders blocks that are read-only by default with a pencil icon per block. In `consolidateMemories()`, a Set tracks which block indices are in edit mode.
|
||||
|
||||
```javascript
|
||||
function renderConsolidatedCards(blocks, editingSet) {
|
||||
return blocks.map((b, bi) => {
|
||||
const isEditing = editingSet.has(bi);
|
||||
const themeLabel = `${bi + 1}. ${b.chat}`;
|
||||
|
||||
if (isEditing) {
|
||||
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 charMemory_editorCard--editing" data-block="${bi}">
|
||||
<div class="charMemory_cardHeader">
|
||||
<input type="text" class="charMemory_editorThemeInput" value="${escapeHtml(b.chat)}" data-block="${bi}" />
|
||||
<span class="charMemory_cardActions">
|
||||
<button class="charMemory_editorToggleEdit menu_button menu_button_icon" data-block="${bi}" title="Done editing"><i class="fa-solid fa-check"></i></button>
|
||||
<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>`;
|
||||
} else {
|
||||
const bullets = b.bullets.map(bullet => `<li>${escapeHtml(bullet)}</li>`).join('');
|
||||
return `<div class="charMemory_card charMemory_editorCard" data-block="${bi}">
|
||||
<div class="charMemory_cardHeader">
|
||||
<strong>${escapeHtml(themeLabel)}</strong>
|
||||
<span class="charMemory_cardActions">
|
||||
<button class="charMemory_editorToggleEdit menu_button menu_button_icon" data-block="${bi}" title="Edit block"><i class="fa-solid fa-pencil"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<ul>${bullets}</ul>
|
||||
</div>`;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Update buildConsolidationDialog**
|
||||
|
||||
- Change headings to "Original Memories" and "Consolidated Memories"
|
||||
- Pass `editingSet` (empty by default)
|
||||
- Use `renderConsolidatedCards` for right pane
|
||||
- Add Block button hidden by default (CSS class `charMemory_editorAddBlock--hidden`)
|
||||
|
||||
```javascript
|
||||
function buildConsolidationDialog(beforeBlocks, beforeCount, consolidatedBlocks, editingSet) {
|
||||
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 afterCount = countMemories(consolidatedBlocks);
|
||||
const hasEditing = editingSet.size > 0;
|
||||
|
||||
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).map(([k, v]) =>
|
||||
`<option value="${k}">${escapeHtml(v.name)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
<details class="charMemory_promptDisclosure charMemory_promptDisclosure--dialog">
|
||||
<summary><small>Show prompt</small></summary>
|
||||
<textarea id="charMemory_dialogPrompt" class="text_pole textarea_compact" rows="4" placeholder="Edit prompt for this strategy..."></textarea>
|
||||
<div class="charMemory_buttonRow">
|
||||
<input type="button" id="charMemory_dialogRestoreDefault" class="menu_button" value="Restore Default" style="display:none;" />
|
||||
</div>
|
||||
</details>
|
||||
<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 Memories</h4>
|
||||
<div class="charMemory_consolidationContent">${renderReadOnlyCards(beforeBlocks)}</div>
|
||||
</div>
|
||||
<div class="charMemory_consolidationPane">
|
||||
<h4>Consolidated Memories</h4>
|
||||
<div class="charMemory_consolidationContent" id="charMemory_editorPane">${renderConsolidatedCards(consolidatedBlocks, editingSet)}</div>
|
||||
<button class="charMemory_editorAddBlock menu_button ${hasEditing ? '' : 'charMemory_editorAddBlock--hidden'}" id="charMemory_editorAddBlock"><i class="fa-solid fa-plus fa-xs"></i> Add Block</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Rewrite consolidateMemories event delegation**
|
||||
|
||||
Replace the edit-mode event handlers in `consolidateMemories()`. Key changes:
|
||||
|
||||
- Add `const editingSet = new Set();` alongside `editorBlocks` and `versionStack`
|
||||
- `refreshEditor` now passes `editingSet` to `renderConsolidatedCards`
|
||||
- Add toggle-edit handler for `.charMemory_editorToggleEdit`:
|
||||
|
||||
```javascript
|
||||
// Toggle edit mode per block
|
||||
$(document).off('click.charMemoryEditorToggle').on('click.charMemoryEditorToggle', '.charMemory_editorToggleEdit', function () {
|
||||
const bi = Number($(this).data('block'));
|
||||
if (editingSet.has(bi)) {
|
||||
editingSet.delete(bi);
|
||||
} else {
|
||||
editingSet.add(bi);
|
||||
}
|
||||
refreshEditor();
|
||||
});
|
||||
```
|
||||
|
||||
- Update `refreshEditor` to show/hide Add Block based on `editingSet.size > 0`:
|
||||
|
||||
```javascript
|
||||
const refreshEditor = () => {
|
||||
$('#charMemory_editorPane').html(renderConsolidatedCards(editorBlocks, editingSet));
|
||||
$('#charMemory_afterCount').text(countMemories(editorBlocks));
|
||||
$('#charMemory_editorAddBlock').toggleClass('charMemory_editorAddBlock--hidden', editingSet.size === 0);
|
||||
};
|
||||
```
|
||||
|
||||
- Add theme input sync handler:
|
||||
|
||||
```javascript
|
||||
$(document).off('input.charMemoryEditorTheme').on('input.charMemoryEditorTheme', '.charMemory_editorThemeInput', function () {
|
||||
const bi = Number($(this).data('block'));
|
||||
if (editorBlocks[bi]) {
|
||||
editorBlocks[bi].chat = $(this).val();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
- Add dialog prompt handlers (for the expandable prompt in the toolbar):
|
||||
|
||||
```javascript
|
||||
// Sync dialog prompt textarea to settings
|
||||
$('#charMemory_dialogPrompt').off('input').on('input', function () {
|
||||
const strategy = $('#charMemory_consolidationDialogStrategy').val();
|
||||
if (!extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
extension_settings[MODULE_NAME].consolidationPrompts = {};
|
||||
}
|
||||
extension_settings[MODULE_NAME].consolidationPrompts[strategy] = $(this).val();
|
||||
$('#charMemory_dialogRestoreDefault').show();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
// Restore default in dialog
|
||||
$('#charMemory_dialogRestoreDefault').off('click').on('click', function () {
|
||||
const strategy = $('#charMemory_consolidationDialogStrategy').val();
|
||||
if (extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
delete extension_settings[MODULE_NAME].consolidationPrompts[strategy];
|
||||
}
|
||||
const preset = CONSOLIDATION_PRESETS[strategy];
|
||||
$('#charMemory_dialogPrompt').val(preset?.prompt || '');
|
||||
$('#charMemory_dialogRestoreDefault').hide();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
// Update dialog prompt when strategy changes
|
||||
$('#charMemory_consolidationDialogStrategy').off('change').on('change', function () {
|
||||
const strategy = $(this).val();
|
||||
const overrides = extension_settings[MODULE_NAME].consolidationPrompts || {};
|
||||
const prompt = overrides[strategy] || CONSOLIDATION_PRESETS[strategy]?.prompt || '';
|
||||
const isCustomized = !!overrides[strategy];
|
||||
$('#charMemory_dialogPrompt').val(prompt);
|
||||
$('#charMemory_dialogRestoreDefault').toggle(isCustomized);
|
||||
});
|
||||
```
|
||||
|
||||
- Clean up the new namespaced events on popup close:
|
||||
|
||||
```javascript
|
||||
$(document).off('click.charMemoryEditorToggle');
|
||||
$(document).off('input.charMemoryEditorTheme');
|
||||
```
|
||||
|
||||
- On re-run, clear `editingSet`:
|
||||
|
||||
```javascript
|
||||
if (newResult) {
|
||||
versionStack.push(currentBlocks);
|
||||
$('#charMemory_undoRerun').prop('disabled', false);
|
||||
editorBlocks = parseMemories(newResult);
|
||||
editingSet.clear();
|
||||
refreshEditor();
|
||||
}
|
||||
```
|
||||
|
||||
- On undo, clear `editingSet`:
|
||||
|
||||
```javascript
|
||||
$('#charMemory_undoRerun').off('click').on('click', () => {
|
||||
if (versionStack.length === 0) return;
|
||||
editorBlocks = versionStack.pop();
|
||||
editingSet.clear();
|
||||
refreshEditor();
|
||||
if (versionStack.length === 0) {
|
||||
$('#charMemory_undoRerun').prop('disabled', true);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Step 4: Add CSS**
|
||||
|
||||
```css
|
||||
.charMemory_editorCard--editing {
|
||||
border: 1px solid var(--SmartThemeBorderColor, rgba(255,255,255,0.15));
|
||||
}
|
||||
|
||||
.charMemory_editorToggleEdit {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.charMemory_editorToggleEdit:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.charMemory_editorThemeInput {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 2px 6px;
|
||||
font-weight: bold;
|
||||
font-size: 0.95em;
|
||||
background: var(--SmartThemeBlurTintColor, rgba(0, 0, 0, 0.05));
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.charMemory_editorThemeInput:focus {
|
||||
border-color: var(--SmartThemeBorderColor, rgba(255,255,255,0.2));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.charMemory_editorAddBlock--hidden {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Remove old renderEditableCards function** (it's replaced by renderConsolidatedCards)
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add index.js style.css
|
||||
git commit -m "feat: read-only cards by default with per-block edit toggle and themed headers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Persistent mini-log at panel bottom
|
||||
|
||||
**Files:**
|
||||
- Modify: `settings.html:284-286` (add mini-log before closing divs)
|
||||
- Modify: `index.js:64-90` (update logActivity / updateActivityLogDisplay to also update mini-log)
|
||||
- Modify: `style.css` (add mini-log styles)
|
||||
|
||||
**Step 1: Add mini-log HTML to settings.html**
|
||||
|
||||
Insert before the closing `</div><!-- inline-drawer-content -->` (before line 286):
|
||||
|
||||
```html
|
||||
<!-- Persistent mini activity log — always visible -->
|
||||
<div class="charMemory_miniLog" id="charMemory_miniLog">
|
||||
<div class="charMemory_miniLogContent" id="charMemory_miniLogContent">
|
||||
<div class="charMemory_diagEmpty charMemory_miniLogEmpty">No activity yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Step 2: Update logActivity to also populate mini-log**
|
||||
|
||||
In `updateActivityLogDisplay()`, after updating the main `#charMemory_activityLog`, also update the mini-log:
|
||||
|
||||
```javascript
|
||||
function updateActivityLogDisplay() {
|
||||
// ... existing main log update code ...
|
||||
|
||||
// Update mini-log (last 3 entries, non-verbose only)
|
||||
const $miniLog = $('#charMemory_miniLogContent');
|
||||
if (!$miniLog.length) return;
|
||||
|
||||
if (activityLog.length === 0) {
|
||||
$miniLog.html('<div class="charMemory_diagEmpty charMemory_miniLogEmpty">No activity yet.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const miniEntries = activityLog.slice(0, 3);
|
||||
const miniHtml = miniEntries.map(entry => {
|
||||
const typeClass = `charMemory_log_${entry.type}`;
|
||||
const msgText = entry.message.split('\n')[0]; // first line only
|
||||
return `<div class="charMemory_logEntry ${typeClass}"><span class="charMemory_logTime">${entry.timestamp}</span> ${escapeHtml(msgText)}</div>`;
|
||||
}).join('');
|
||||
$miniLog.html(miniHtml);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Add CSS for mini-log**
|
||||
|
||||
```css
|
||||
.charMemory_miniLog {
|
||||
border-top: 1px solid var(--SmartThemeBorderColor, rgba(255,255,255,0.1));
|
||||
margin-top: 8px;
|
||||
padding-top: 4px;
|
||||
font-size: 0.8em;
|
||||
font-family: monospace;
|
||||
max-height: 60px;
|
||||
overflow-y: hidden;
|
||||
cursor: pointer;
|
||||
transition: max-height 0.2s ease;
|
||||
}
|
||||
|
||||
.charMemory_miniLog:hover,
|
||||
.charMemory_miniLog.charMemory_miniLog--expanded {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.charMemory_miniLogEmpty {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Add click-to-expand behavior**
|
||||
|
||||
In `setupListeners()`:
|
||||
|
||||
```javascript
|
||||
$('#charMemory_miniLog').off('click').on('click', function () {
|
||||
$(this).toggleClass('charMemory_miniLog--expanded');
|
||||
});
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add settings.html index.js style.css
|
||||
git commit -m "feat: add persistent mini activity log at panel bottom"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Final integration, cleanup, and changelog
|
||||
|
||||
**Files:**
|
||||
- Modify: `index.js` (remove dead code, migrate old `consolidationPrompt` setting)
|
||||
- Modify: `CHANGELOG.md`
|
||||
|
||||
**Step 1: Add settings migration**
|
||||
|
||||
In `loadSettings()`, migrate old `consolidationPrompt` field to new `consolidationPrompts`:
|
||||
|
||||
```javascript
|
||||
// Migrate old consolidationPrompt to new per-preset system
|
||||
if (extension_settings[MODULE_NAME].consolidationPrompt && !extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
const oldPrompt = extension_settings[MODULE_NAME].consolidationPrompt;
|
||||
const oldStrategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
extension_settings[MODULE_NAME].consolidationPrompts = { [oldStrategy]: oldPrompt };
|
||||
delete extension_settings[MODULE_NAME].consolidationPrompt;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Remove dead code**
|
||||
|
||||
- Remove old `renderEditableCards` function if not already removed in Task 4
|
||||
- Remove any remaining references to the `custom` preset
|
||||
- Remove old `consolidationPrompt` from `defaultSettings`
|
||||
|
||||
**Step 3: Update CHANGELOG.md**
|
||||
|
||||
Add under `## 1.3.0 > ### Improvements`:
|
||||
|
||||
```markdown
|
||||
- **Read-only consolidation preview**: Consolidated memories now display as clean read-only cards by default, matching the original memories pane. Click the pencil icon on any block to enter edit mode for that block.
|
||||
- **Themed block headers**: The LLM now groups consolidated memories by theme (e.g., "Relationship History", "Key Events"). Theme names are editable.
|
||||
- **Editable strategy presets**: Each consolidation strategy (Conservative, Balanced, Aggressive) now has an expandable prompt viewer. Customize any preset's prompt and save it — with Restore Default to revert.
|
||||
- **Persistent activity log**: A compact activity log is always visible at the bottom of the panel, regardless of which tab is active. Click to expand.
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add index.js CHANGELOG.md
|
||||
git commit -m "feat: complete UX refinements — migration, cleanup, changelog"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task Summary
|
||||
|
||||
| Task | Description | Files |
|
||||
|------|-------------|-------|
|
||||
| 1 | Editable presets — remove Custom, add per-preset storage | `index.js` |
|
||||
| 2 | Consolidate tab HTML — expandable prompt viewer | `settings.html`, `index.js`, `style.css` |
|
||||
| 3 | Themed block headers — update prompt and parsing | `index.js` |
|
||||
| 4 | Read-only default with per-block edit toggle | `index.js`, `style.css` |
|
||||
| 5 | Persistent mini-log at panel bottom | `settings.html`, `index.js`, `style.css` |
|
||||
| 6 | Final integration, cleanup, changelog | `index.js`, `CHANGELOG.md` |
|
||||
105
docs/plans/2026-02-18-group-chat-roadmap.md
Normal file
105
docs/plans/2026-02-18-group-chat-roadmap.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Roadmap: Group Chat Memory Support
|
||||
|
||||
**Date**: 2026-02-18
|
||||
**Status**: Future roadmap item
|
||||
|
||||
## Problem
|
||||
|
||||
SillyTavern group chats present a memory challenge. CharMemory stores memories as Data Bank files and relies on Vector Storage for retrieval. In group chats:
|
||||
|
||||
- **Per-character Data Bank files exist** — SillyTavern switches `this_chid` for each character's turn, so each character's attachments load correctly.
|
||||
- **Vector Storage queries once for the whole group** — it does NOT re-query per character. All group members get the same retrieved chunks injected, regardless of whose memories they are.
|
||||
- **Character lorebooks DO work per-character in groups** — SillyTavern reloads each character's bound lorebook for their turn.
|
||||
|
||||
This means Data Bank + Vector Storage gives no character-level scoping in group chats. Character A's private memories could be injected into Character B's generation.
|
||||
|
||||
## Requirements for Group Memory
|
||||
|
||||
1. **Private memories** — things only that character knows (backstory reveals, private conversations)
|
||||
2. **Shared memories** — events that happened in the group that everyone witnessed
|
||||
3. **Partial knowledge** — Character A and B were in a scene but C wasn't
|
||||
|
||||
## Approach: Dual-Write (Data Bank + Lorebook)
|
||||
|
||||
Keep the Data Bank as the primary storage (proven, simple, works for 1:1 chats). Add optional lorebook dual-write that unlocks group chat compatibility.
|
||||
|
||||
### How It Works
|
||||
|
||||
- **Data Bank** (existing): CharMemory writes memories to character Data Bank files as it does today. Vector Storage handles retrieval for 1:1 chats.
|
||||
- **Lorebook** (new, opt-in): When enabled, CharMemory also writes each memory block as a lorebook entry. Since SillyTavern's lorebook system is per-character in group chats, each character only sees their own memory entries during their generation turn.
|
||||
|
||||
### User Setting
|
||||
|
||||
A toggle in Settings: "Also write to lorebook" (default: off). When enabled, every extraction writes to both Data Bank and lorebook. The Data Bank file remains the source of truth; lorebook entries are a derived copy for retrieval.
|
||||
|
||||
## MemoryBooks Compatibility
|
||||
|
||||
The [SillyTavern-MemoryBooks](https://github.com/aikohanasaki/SillyTavern-MemoryBooks) extension uses lorebook entries for memory storage. CharMemory's lorebook entries should be compatible with MemoryBooks' format so the two extensions can coexist.
|
||||
|
||||
### MemoryBooks Entry Format
|
||||
|
||||
MemoryBooks identifies its entries with:
|
||||
- `stmemorybooks: true` flag
|
||||
- Numbered titles: `[001] - Title`
|
||||
- Keywords array for activation
|
||||
- `vectorized: true`, `selective: true` for retrieval
|
||||
- Standard lorebook fields (`position: 0`, `depth: 4`, `probability: 100`, etc.)
|
||||
|
||||
### Mapping CharMemory Blocks to Lorebook Entries
|
||||
|
||||
| CharMemory | Lorebook entry field |
|
||||
|---|---|
|
||||
| `block.chat` (theme/chat name) | `comment` (entry title, with `[NNN]` prefix) |
|
||||
| `block.bullets.join('\n')` | `content` |
|
||||
| Block index | `[001]`, `[002]`, etc. numbering |
|
||||
| `block.date` | Could include in title template |
|
||||
| TBD | `key` (activation keywords) |
|
||||
|
||||
### SillyTavern APIs Needed
|
||||
|
||||
```javascript
|
||||
import { createWorldInfoEntry, saveWorldInfo, loadWorldInfo } from '../../../world-info.js';
|
||||
```
|
||||
|
||||
These are the same APIs MemoryBooks uses. CharMemory would call them after its existing `writeMemories()` to Data Bank.
|
||||
|
||||
## Open Questions
|
||||
|
||||
### Keyword Extraction
|
||||
|
||||
MemoryBooks entries use keyword arrays for lorebook activation. CharMemory doesn't extract keywords today. Options to explore later:
|
||||
1. Add keyword extraction to the extraction prompt
|
||||
2. Auto-derive keywords from bullet text (no extra LLM call)
|
||||
3. Rely on vectorized mode only (keywords optional)
|
||||
|
||||
### Private vs Shared Memories in Groups
|
||||
|
||||
When extracting from a group chat, how to determine which memories are private to one character vs shared knowledge:
|
||||
- All memories from group chat could be shared (simplest)
|
||||
- Character-specific memories could be tagged by who was "speaking" when the event occurred
|
||||
- User could manually scope memories after extraction
|
||||
|
||||
### Consolidation Sync
|
||||
|
||||
When the user consolidates memories (which modifies the Data Bank file), the lorebook entries would need to be regenerated to stay in sync. Options:
|
||||
- Re-sync lorebook entries after every consolidation
|
||||
- Treat lorebook as append-only and only sync on explicit user action
|
||||
- Delete and recreate lorebook entries from the Data Bank file on demand
|
||||
|
||||
### Existing CharMemory Data Bank Files
|
||||
|
||||
Users with existing memories in Data Bank files would need a migration path:
|
||||
- "Sync to Lorebook" button that reads all current memories and creates lorebook entries
|
||||
- Could be one-time or repeatable
|
||||
|
||||
## What We Can Reuse
|
||||
|
||||
The entire extraction pipeline is storage-agnostic:
|
||||
- Provider system and all supported APIs
|
||||
- Extraction prompt and chunk processing
|
||||
- Consolidation (themed blocks, presets, preview/undo)
|
||||
- Batch extraction
|
||||
- Activity log, diagnostics, settings UI
|
||||
- Memory format (`<memory>` blocks with bullets) as internal representation
|
||||
|
||||
Only the storage layer needs a new backend. The extraction pipeline produces structured memory blocks — it doesn't care where they end up.
|
||||
41
docs/plans/2026-02-18-merge-toggle-datetime-design.md
Normal file
41
docs/plans/2026-02-18-merge-toggle-datetime-design.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Design: Optional Merge Behavior & Date/Time in Extraction
|
||||
|
||||
**Date**: 2026-02-18
|
||||
**Status**: Approved
|
||||
|
||||
## Problem
|
||||
|
||||
1. **mergeMemoryBlocks creates huge blocks**: For long chats (800+ messages), every extraction chunk's output shares the same chat ID and gets merged into a single block with 200+ bullets. This block is too large for the consolidation LLM to process, making consolidation unusable for the chats that need it most.
|
||||
|
||||
2. **Memories lack temporal context**: The extraction prompt doesn't encourage capturing dates and times. Memories like "She visited Paris" lose valuable temporal information that was present in the conversation.
|
||||
|
||||
## Change 1: Optional Merge Behavior
|
||||
|
||||
**Setting**: "Merge extraction chunks" checkbox in Settings tab under Extraction Settings.
|
||||
- Stored as `extension_settings.charMemory.mergeChunks`
|
||||
- **Default: off**
|
||||
|
||||
**Behavior when off**: After multi-chunk extraction, each chunk's `<memory>` block stays separate. Long chats produce multiple smaller blocks.
|
||||
|
||||
**Behavior when on**: Current behavior — `mergeMemoryBlocks` runs after multi-chunk extraction, combining blocks with the same chat ID.
|
||||
|
||||
**Helper text**: "When enabled, extraction results from the same chat are merged into a single block. Disable for long chats to keep blocks smaller for consolidation."
|
||||
|
||||
**Location in UI**: Settings tab, under "Extraction Settings" section, after the "Max response length" slider.
|
||||
|
||||
## Change 2: Date/Time Gentle Nudge
|
||||
|
||||
Add one line to the `WHAT TO EXTRACT` bullet list in `defaultExtractionPrompt`:
|
||||
|
||||
```
|
||||
- Dates and times when mentioned or clearly implied in the conversation
|
||||
```
|
||||
|
||||
This is a gentle nudge — the LLM includes temporal context when it naturally appears but doesn't force-prefix every bullet.
|
||||
|
||||
Only affects the default prompt. Users with customized prompts are not affected (their override is preserved).
|
||||
|
||||
## Files to Modify
|
||||
|
||||
- `index.js`: Add `mergeChunks: false` to defaultSettings, wrap `mergeMemoryBlocks` call in conditional, add date/time line to `defaultExtractionPrompt`
|
||||
- `settings.html`: Add merge checkbox in Settings tab
|
||||
581
index.js
581
index.js
|
|
@ -73,22 +73,38 @@ function logActivity(message, type = 'info') {
|
|||
|
||||
function updateActivityLogDisplay() {
|
||||
const $container = $('#charMemory_activityLog');
|
||||
if (!$container.length) return;
|
||||
if ($container.length) {
|
||||
if (activityLog.length === 0) {
|
||||
$container.html('<div class="charMemory_diagEmpty">No activity yet.</div>');
|
||||
} else {
|
||||
const html = activityLog.map(entry => {
|
||||
const typeClass = `charMemory_log_${entry.type}`;
|
||||
const isVerbose = entry.message.includes('\n');
|
||||
const msgHtml = isVerbose
|
||||
? `<details><summary>${escapeHtml(entry.message.split('\n')[0])}</summary><pre class="charMemory_logVerbose">${escapeHtml(entry.message)}</pre></details>`
|
||||
: escapeHtml(entry.message);
|
||||
return `<div class="charMemory_logEntry ${typeClass}"><span class="charMemory_logTime">${entry.timestamp}</span> ${msgHtml}</div>`;
|
||||
}).join('');
|
||||
$container.html(html);
|
||||
}
|
||||
}
|
||||
|
||||
// Update mini-log (last 3 entries, first line only)
|
||||
const $miniLog = $('#charMemory_miniLogContent');
|
||||
if (!$miniLog.length) return;
|
||||
|
||||
if (activityLog.length === 0) {
|
||||
$container.html('<div class="charMemory_diagEmpty">No activity yet.</div>');
|
||||
$miniLog.html('<div class="charMemory_diagEmpty charMemory_miniLogEmpty">No activity yet.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const html = activityLog.map(entry => {
|
||||
const miniEntries = activityLog.slice(0, 3);
|
||||
const miniHtml = miniEntries.map(entry => {
|
||||
const typeClass = `charMemory_log_${entry.type}`;
|
||||
const isVerbose = entry.message.includes('\n');
|
||||
const msgHtml = isVerbose
|
||||
? `<details><summary>${escapeHtml(entry.message.split('\n')[0])}</summary><pre class="charMemory_logVerbose">${escapeHtml(entry.message)}</pre></details>`
|
||||
: escapeHtml(entry.message);
|
||||
return `<div class="charMemory_logEntry ${typeClass}"><span class="charMemory_logTime">${entry.timestamp}</span> ${msgHtml}</div>`;
|
||||
const msgText = entry.message.split('\n')[0];
|
||||
return `<div class="charMemory_logEntry ${typeClass}"><span class="charMemory_logTime">${entry.timestamp}</span> ${escapeHtml(msgText)}</div>`;
|
||||
}).join('');
|
||||
$container.html(html);
|
||||
$miniLog.html(miniHtml);
|
||||
}
|
||||
|
||||
const defaultExtractionPrompt = `You are a memory extraction assistant. Read the recent chat messages and identify the most significant facts, events, and developments worth remembering long-term.
|
||||
|
|
@ -125,6 +141,7 @@ WHAT TO EXTRACT — ask for each item: "Would {{char}} bring this up unprompted
|
|||
- Significant events and their outcomes (not the step-by-step process)
|
||||
- Skills, possessions, or status changes
|
||||
- Emotional turning points
|
||||
- Dates and times when mentioned or clearly implied in the conversation
|
||||
|
||||
DO NOT EXTRACT:
|
||||
- Anything already described in the CHARACTER CARD above — traits, profession, appearance, personality, habits, preferences, or abilities that are baseline knowledge. This includes rephrasing card traits as discoveries (e.g. if the card says "exhibitionist", do not write "she admitted that being watched turns her on")
|
||||
|
|
@ -356,7 +373,10 @@ const defaultSettings = {
|
|||
interval: 20,
|
||||
maxMessagesPerExtraction: 20,
|
||||
responseLength: 1000,
|
||||
mergeChunks: false,
|
||||
extractionPrompt: defaultExtractionPrompt,
|
||||
consolidationStrategy: 'balanced',
|
||||
consolidationPrompts: {},
|
||||
source: EXTRACTION_SOURCE.PROVIDER,
|
||||
fileName: DEFAULT_FILE_NAME,
|
||||
perChat: false,
|
||||
|
|
@ -418,8 +438,8 @@ function parseMemories(content) {
|
|||
// Extract chat and date attributes
|
||||
const chatMatch = attrs.match(/chat="([^"]*)"/);
|
||||
const dateMatch = attrs.match(/date="([^"]*)"/);
|
||||
const chat = chatMatch ? chatMatch[1] : 'unknown';
|
||||
const date = dateMatch ? dateMatch[1] : '';
|
||||
const chat = chatMatch ? unescapeAttr(chatMatch[1]) : 'unknown';
|
||||
const date = dateMatch ? unescapeAttr(dateMatch[1]) : '';
|
||||
|
||||
// Extract bullets (lines starting with "- ")
|
||||
const bullets = body.split('\n')
|
||||
|
|
@ -450,10 +470,18 @@ function countMemories(blocks) {
|
|||
* @param {{chat: string, date: string, bullets: string[]}[]} blocks
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeAttr(text) {
|
||||
return String(text).replace(/&/g, '&').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function unescapeAttr(text) {
|
||||
return String(text).replace(/"/g, '"').replace(/&/g, '&');
|
||||
}
|
||||
|
||||
function serializeMemories(blocks) {
|
||||
return blocks.map(b => {
|
||||
const bulletsText = b.bullets.map(bullet => `- ${bullet}`).join('\n');
|
||||
return `<memory chat="${b.chat}" date="${b.date}">\n${bulletsText}\n</memory>`;
|
||||
return `<memory chat="${escapeAttr(b.chat)}" date="${escapeAttr(b.date)}">\n${bulletsText}\n</memory>`;
|
||||
}).join('\n\n');
|
||||
}
|
||||
|
||||
|
|
@ -556,6 +584,22 @@ function toggleProviderSettings(source) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the consolidation strategy UI: show custom textarea or preset preview.
|
||||
*/
|
||||
function updateConsolidationStrategyUI() {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
const overrides = extension_settings[MODULE_NAME].consolidationPrompts || {};
|
||||
const currentPrompt = overrides[strategy] || CONSOLIDATION_PRESETS[strategy]?.prompt || '';
|
||||
const isCustomized = !!overrides[strategy];
|
||||
|
||||
$('#charMemory_consolidationPrompt').val(currentPrompt);
|
||||
$('#charMemory_restorePresetDefault').toggle(isCustomized);
|
||||
|
||||
const previewText = isCustomized ? `${CONSOLIDATION_PRESETS[strategy]?.name} (customized)` : CONSOLIDATION_PRESETS[strategy]?.description || '';
|
||||
$('#charMemory_consolidationPreview').text(previewText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the provider preset dropdown from PROVIDER_PRESETS.
|
||||
*/
|
||||
|
|
@ -768,6 +812,20 @@ function loadSettings() {
|
|||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
// Migrate old consolidationPrompt to new per-preset system
|
||||
if (extension_settings[MODULE_NAME].consolidationPrompt) {
|
||||
const oldPrompt = extension_settings[MODULE_NAME].consolidationPrompt;
|
||||
const oldStrategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
if (!extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
extension_settings[MODULE_NAME].consolidationPrompts = {};
|
||||
}
|
||||
if (!extension_settings[MODULE_NAME].consolidationPrompts[oldStrategy]) {
|
||||
extension_settings[MODULE_NAME].consolidationPrompts[oldStrategy] = oldPrompt;
|
||||
}
|
||||
delete extension_settings[MODULE_NAME].consolidationPrompt;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
// Migrate NanoGPT source → provider system
|
||||
if (extension_settings[MODULE_NAME].source === 'nanogpt') {
|
||||
extension_settings[MODULE_NAME].source = EXTRACTION_SOURCE.PROVIDER;
|
||||
|
|
@ -791,6 +849,7 @@ function loadSettings() {
|
|||
|
||||
// Bind UI elements to settings
|
||||
$('#charMemory_enabled').prop('checked', extension_settings[MODULE_NAME].enabled);
|
||||
$('#charMemory_mergeChunks').prop('checked', extension_settings[MODULE_NAME].mergeChunks);
|
||||
$('#charMemory_perChat').prop('checked', extension_settings[MODULE_NAME].perChat);
|
||||
$('#charMemory_interval').val(extension_settings[MODULE_NAME].interval);
|
||||
$('#charMemory_intervalCounter').val(extension_settings[MODULE_NAME].interval);
|
||||
|
|
@ -803,6 +862,8 @@ function loadSettings() {
|
|||
$('#charMemory_extractionPrompt').val(extension_settings[MODULE_NAME].extractionPrompt);
|
||||
$('#charMemory_groupMode').val(extension_settings[MODULE_NAME].groupExtractionMode);
|
||||
$('#charMemory_groupExtractionPrompt').val(extension_settings[MODULE_NAME].groupExtractionPrompt);
|
||||
$('#charMemory_consolidationStrategy').val(extension_settings[MODULE_NAME].consolidationStrategy || 'balanced');
|
||||
updateConsolidationStrategyUI();
|
||||
$('#charMemory_source').val(extension_settings[MODULE_NAME].source);
|
||||
$('#charMemory_fileName').val(extension_settings[MODULE_NAME].fileName);
|
||||
$('#charMemory_verboseLog').prop('checked', extension_settings[MODULE_NAME].verboseLogging);
|
||||
|
|
@ -2366,8 +2427,8 @@ async function extractMemories({
|
|||
chunksProcessed++;
|
||||
}
|
||||
|
||||
// Merge blocks with the same chat ID + date (from multi-chunk extraction)
|
||||
if (chunksProcessed > 1 && totalMemories > 0) {
|
||||
// Merge blocks with the same chat ID (from multi-chunk extraction)
|
||||
if (chunksProcessed > 1 && totalMemories > 0 && extension_settings[MODULE_NAME].mergeChunks) {
|
||||
const allBlocks = parseMemories(await readMemories());
|
||||
const merged = mergeMemoryBlocks(allBlocks);
|
||||
if (merged.length < allBlocks.length) {
|
||||
|
|
@ -2794,9 +2855,12 @@ function updateDiagnosticsDisplay() {
|
|||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// ============ Memory Manager ============
|
||||
|
|
@ -2963,20 +3027,105 @@ async function deleteBlock(blockIndex) {
|
|||
|
||||
// ============ Consolidation ============
|
||||
|
||||
function buildConsolidationPreview(beforeBlocks, afterBlocks, beforeCount, afterCount) {
|
||||
const renderSection = (title, blocks, count) => {
|
||||
const cards = blocks.map(b => {
|
||||
/**
|
||||
* Re-index editingSet after a block is removed via splice.
|
||||
* Indices above the removed position shift down by one.
|
||||
*/
|
||||
function reindexEditingSet(editingSet, removedIndex) {
|
||||
const updated = new Set();
|
||||
for (const idx of editingSet) {
|
||||
if (idx < removedIndex) updated.add(idx);
|
||||
else if (idx > removedIndex) updated.add(idx - 1);
|
||||
}
|
||||
editingSet.clear();
|
||||
for (const idx of updated) editingSet.add(idx);
|
||||
}
|
||||
|
||||
function renderConsolidatedCards(blocks, editingSet) {
|
||||
return blocks.map((b, bi) => {
|
||||
const isEditing = editingSet.has(bi);
|
||||
const themeLabel = `${bi + 1}. ${b.chat}`;
|
||||
|
||||
if (isEditing) {
|
||||
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 charMemory_editorCard--editing" data-block="${bi}">
|
||||
<div class="charMemory_cardHeader">
|
||||
<input type="text" class="charMemory_editorThemeInput" value="${escapeHtml(b.chat)}" data-block="${bi}" />
|
||||
<span class="charMemory_cardActions">
|
||||
<button class="charMemory_editorToggleEdit menu_button menu_button_icon" data-block="${bi}" title="Done editing"><i class="fa-solid fa-check"></i></button>
|
||||
<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>`;
|
||||
} else {
|
||||
const bullets = b.bullets.map(bullet => `<li>${escapeHtml(bullet)}</li>`).join('');
|
||||
return `<div class="charMemory_card charMemory_editorCard" data-block="${bi}">
|
||||
<div class="charMemory_cardHeader">
|
||||
<strong>${escapeHtml(themeLabel)}</strong>
|
||||
<span class="charMemory_cardActions">
|
||||
<button class="charMemory_editorToggleEdit menu_button menu_button_icon" data-block="${bi}" title="Edit block"><i class="fa-solid fa-pencil"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
<ul>${bullets}</ul>
|
||||
</div>`;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function buildConsolidationDialog(beforeBlocks, beforeCount, consolidatedBlocks, editingSet) {
|
||||
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('');
|
||||
return `<h3>${title} (${count} memories)</h3>${cards}`;
|
||||
};
|
||||
return `<div style="display:flex;gap:1em;">
|
||||
<div style="flex:1;overflow-y:auto;max-height:60vh;">${renderSection('Before', beforeBlocks, beforeCount)}</div>
|
||||
<div style="flex:1;overflow-y:auto;max-height:60vh;">${renderSection('After', afterBlocks, afterCount)}</div>
|
||||
|
||||
const afterCount = countMemories(consolidatedBlocks);
|
||||
const hasEditing = editingSet.size > 0;
|
||||
|
||||
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).map(([k, v]) =>
|
||||
`<option value="${k}">${escapeHtml(v.name)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
<details class="charMemory_promptDisclosure charMemory_promptDisclosure--dialog">
|
||||
<summary><small>Show prompt</small></summary>
|
||||
<textarea id="charMemory_dialogPrompt" class="text_pole textarea_compact" rows="4" placeholder="Edit prompt for this strategy..."></textarea>
|
||||
<div class="charMemory_buttonRow">
|
||||
<input type="button" id="charMemory_dialogRestoreDefault" class="menu_button" value="Restore Default" style="display:none;" />
|
||||
</div>
|
||||
</details>
|
||||
<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 Memories</h4>
|
||||
<div class="charMemory_consolidationContent">${renderReadOnlyCards(beforeBlocks)}</div>
|
||||
</div>
|
||||
<div class="charMemory_consolidationPane">
|
||||
<h4>Consolidated Memories</h4>
|
||||
<div class="charMemory_consolidationContent" id="charMemory_editorPane">${renderConsolidatedCards(consolidatedBlocks, editingSet)}</div>
|
||||
<button class="charMemory_editorAddBlock menu_button ${hasEditing ? '' : 'charMemory_editorAddBlock--hidden'}" id="charMemory_editorAddBlock"><i class="fa-solid fa-plus fa-xs"></i> Add Block</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -2994,58 +3143,66 @@ async function undoConsolidation() {
|
|||
updateStatusDisplay();
|
||||
}
|
||||
|
||||
const consolidationPrompt = `You are a memory consolidation assistant. Review the following character memories and consolidate them.
|
||||
const CONSOLIDATION_PRESETS = {
|
||||
conservative: {
|
||||
name: 'Conservative',
|
||||
description: 'Only merge near-exact duplicates. Preserves everything else.',
|
||||
prompt: `Merge ONLY near-exact duplicate memories. If two bullets say essentially the same thing, keep the more detailed version. Do NOT combine loosely related facts. Do NOT summarize. Preserve every distinct piece of information.`,
|
||||
},
|
||||
balanced: {
|
||||
name: 'Balanced',
|
||||
description: 'Merge duplicates and combine related facts.',
|
||||
prompt: `Merge duplicate or near-duplicate memories into one. Combine closely related facts about the same event or topic. Preserve all unique information — do NOT discard distinct memories. Summarize in third person.`,
|
||||
},
|
||||
aggressive: {
|
||||
name: 'Aggressive',
|
||||
description: 'Compress heavily. Summarize themes. Minimize bullet count.',
|
||||
prompt: `Aggressively consolidate these memories into the fewest possible entries. Group by theme or topic. Summarize rather than listing individual events. It's OK to lose minor details if the key facts are preserved. Aim for a compact overview.`,
|
||||
},
|
||||
};
|
||||
|
||||
function buildConsolidationPrompt(memoriesText) {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
const overrides = extension_settings[MODULE_NAME].consolidationPrompts || {};
|
||||
const userPrompt = overrides[strategy]
|
||||
|| CONSOLIDATION_PRESETS[strategy]?.prompt
|
||||
|| CONSOLIDATION_PRESETS.balanced.prompt;
|
||||
return `You are a memory consolidation assistant. Review the following character memories and consolidate them.
|
||||
|
||||
RULES:
|
||||
1. Merge duplicate or near-duplicate memories into one.
|
||||
2. Combine closely related facts about the same event or topic.
|
||||
3. Preserve all unique information — do NOT discard distinct memories.
|
||||
4. Summarize in third person. Do NOT copy text verbatim from the input.
|
||||
5. Do NOT use emojis anywhere in the output.
|
||||
6. Each consolidated memory must be wrapped in <memory></memory> tags.
|
||||
7. Inside each <memory> block, use a markdown bulleted list (lines starting with "- ").
|
||||
${userPrompt}
|
||||
|
||||
ADDITIONAL FORMAT RULES:
|
||||
1. Do NOT use emojis anywhere in the output.
|
||||
2. Do NOT copy text verbatim from the input — rephrase in third person.
|
||||
3. Group memories by theme. Each group is wrapped in <memory chat="Theme Name"></memory> tags where "Theme Name" is a short descriptive label (e.g. "Relationship History", "Character Background", "Key Events").
|
||||
4. Inside each <memory> block, use a markdown bulleted list (lines starting with "- ").
|
||||
|
||||
MEMORIES TO CONSOLIDATE:
|
||||
{{memories}}
|
||||
${memoriesText}
|
||||
|
||||
Output ONLY <memory> blocks. No headers, no commentary, no extra text.`;
|
||||
}
|
||||
|
||||
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`);
|
||||
|
||||
async function runConsolidationLLM(memories) {
|
||||
let memoriesText = memories.map((b, i) =>
|
||||
`[Block ${i + 1}]\n${b.bullets.map(bullet => `- ${bullet}`).join('\n')}`,
|
||||
).join('\n\n');
|
||||
|
||||
// Truncate for WebLLM's smaller context window
|
||||
const isWebLlm = extension_settings[MODULE_NAME].source === EXTRACTION_SOURCE.WEBLLM;
|
||||
if (isWebLlm) {
|
||||
const templateLength = consolidationPrompt.replace('{{memories}}', '').length;
|
||||
const available = Math.max(WEBLLM_MAX_PROMPT_CHARS - templateLength, 1000);
|
||||
const template = buildConsolidationPrompt('');
|
||||
const available = Math.max(WEBLLM_MAX_PROMPT_CHARS - template.length, 1000);
|
||||
memoriesText = truncateText(memoriesText, available);
|
||||
}
|
||||
|
||||
let prompt = consolidationPrompt.replace('{{memories}}', memoriesText);
|
||||
let prompt = buildConsolidationPrompt(memoriesText);
|
||||
prompt = substituteParamsExtended(prompt);
|
||||
|
||||
try {
|
||||
inApiCall = true;
|
||||
const sourceLabel = getSourceLabel();
|
||||
toastr.info(`Consolidating ${beforeCount} memories via ${sourceLabel}...`, 'CharMemory', { timeOut: 3000 });
|
||||
toastr.info(`Consolidating via ${sourceLabel}...`, 'CharMemory', { timeOut: 3000 });
|
||||
|
||||
const verbose = extension_settings[MODULE_NAME].verboseLogging;
|
||||
if (verbose) {
|
||||
|
|
@ -3071,52 +3228,286 @@ async function consolidateMemories() {
|
|||
|
||||
if (!cleanResult) {
|
||||
logActivity('Consolidation returned empty result', 'warning');
|
||||
toastr.warning('Consolidation returned empty result. Memories unchanged.', 'CharMemory');
|
||||
return;
|
||||
toastr.warning('Consolidation returned empty result.', 'CharMemory');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse into memory format, then serialize back to plain text for the editor
|
||||
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')}`;
|
||||
|
||||
const consolidationRegex = /<memory>([\s\S]*?)<\/memory>/gi;
|
||||
const consolidationRegex = /<memory(?:\s+chat="([^"]*)")?>([\s\S]*?)<\/memory>/gi;
|
||||
const consolidationMatches = [...cleanResult.matchAll(consolidationRegex)];
|
||||
const rawEntries = consolidationMatches.length > 0
|
||||
? consolidationMatches.map(m => m[1].trim()).filter(Boolean)
|
||||
: [cleanResult.trim()].filter(Boolean);
|
||||
? consolidationMatches.map(m => ({ theme: m[1] || 'Consolidated', content: m[2].trim() })).filter(e => e.content)
|
||||
: [{ theme: 'Consolidated', content: cleanResult.trim() }].filter(e => e.content);
|
||||
|
||||
const consolidated = rawEntries.map(entry => {
|
||||
const bullets = entry.split('\n')
|
||||
const bullets = entry.content.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l.startsWith('- '))
|
||||
.map(l => l.slice(2).trim())
|
||||
.filter(Boolean);
|
||||
return { chat: 'consolidated', date: timestamp, bullets: bullets.length > 0 ? bullets : [entry] };
|
||||
return { chat: entry.theme, date: timestamp, bullets: bullets.length > 0 ? bullets : [entry.content] };
|
||||
});
|
||||
|
||||
const afterCount = countMemories(consolidated);
|
||||
const previewHtml = buildConsolidationPreview(memories, consolidated, beforeCount, afterCount);
|
||||
const confirmed = await callGenericPopup(previewHtml, POPUP_TYPE.CONFIRM, '', { wide: true, allowVerticalScrolling: true });
|
||||
if (!confirmed) {
|
||||
logActivity('Consolidation cancelled by user');
|
||||
toastr.info('Consolidation cancelled.', 'CharMemory');
|
||||
return;
|
||||
}
|
||||
|
||||
consolidationBackup = content;
|
||||
await writeMemories(serializeMemories(consolidated));
|
||||
$('#charMemory_undoConsolidate').prop('disabled', false);
|
||||
logActivity(`Consolidation complete: ${beforeCount} → ${afterCount} memories`, 'success');
|
||||
toastr.success(`Consolidated ${beforeCount} → ${afterCount} memories.`, 'CharMemory');
|
||||
updateStatusDisplay();
|
||||
return serializeMemories(consolidated);
|
||||
} catch (err) {
|
||||
console.error(LOG_PREFIX, 'Consolidation failed:', err);
|
||||
logActivity(`Consolidation failed: ${err.message}`, 'error');
|
||||
toastr.error('Memory consolidation failed. Check console for details.', 'CharMemory');
|
||||
return null;
|
||||
} finally {
|
||||
inApiCall = false;
|
||||
}
|
||||
}
|
||||
|
||||
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`);
|
||||
|
||||
// Show busy state on button
|
||||
const $btn = $('#charMemory_consolidate');
|
||||
$btn.val('Consolidating…').prop('disabled', true);
|
||||
|
||||
// Run initial consolidation — returns serialized text, parse to blocks
|
||||
let initialResult;
|
||||
try {
|
||||
initialResult = await runConsolidationLLM(memories);
|
||||
} finally {
|
||||
$btn.val('Consolidate').prop('disabled', false);
|
||||
}
|
||||
if (!initialResult) return;
|
||||
|
||||
let editorBlocks = parseMemories(initialResult);
|
||||
const versionStack = [];
|
||||
const editingSet = new Set();
|
||||
|
||||
// Deep copy blocks array
|
||||
const cloneBlocks = (blocks) => blocks.map(b => ({ ...b, bullets: [...b.bullets] }));
|
||||
|
||||
// Re-render the editor pane from editorBlocks
|
||||
const refreshEditor = () => {
|
||||
$('#charMemory_editorPane').html(renderConsolidatedCards(editorBlocks, editingSet));
|
||||
$('#charMemory_afterCount').text(countMemories(editorBlocks));
|
||||
$('#charMemory_editorAddBlock').toggleClass('charMemory_editorAddBlock--hidden', editingSet.size === 0);
|
||||
};
|
||||
|
||||
// Build and show the interactive dialog
|
||||
const dialogHtml = buildConsolidationDialog(memories, beforeCount, editorBlocks, editingSet);
|
||||
const popup = callGenericPopup(dialogHtml, POPUP_TYPE.CONFIRM, '', { wide: true, allowVerticalScrolling: true });
|
||||
|
||||
// Set up the strategy dropdown and prompt viewer to match current setting
|
||||
const currentStrategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
$('#charMemory_consolidationDialogStrategy').val(currentStrategy);
|
||||
const overrides = extension_settings[MODULE_NAME].consolidationPrompts || {};
|
||||
const currentPrompt = overrides[currentStrategy] || CONSOLIDATION_PRESETS[currentStrategy]?.prompt || '';
|
||||
$('#charMemory_dialogPrompt').val(currentPrompt);
|
||||
$('#charMemory_dialogRestoreDefault').toggle(!!overrides[currentStrategy]);
|
||||
|
||||
// === Event delegation for editor interactions ===
|
||||
|
||||
// Toggle edit mode per block
|
||||
$(document).off('click.charMemoryEditorToggle').on('click.charMemoryEditorToggle', '.charMemory_editorToggleEdit', function () {
|
||||
const bi = Number($(this).data('block'));
|
||||
if (editingSet.has(bi)) {
|
||||
editingSet.delete(bi);
|
||||
} else {
|
||||
editingSet.add(bi);
|
||||
}
|
||||
refreshEditor();
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
|
||||
// Sync theme input changes back to editorBlocks
|
||||
$(document).off('input.charMemoryEditorTheme').on('input.charMemoryEditorTheme', '.charMemory_editorThemeInput', function () {
|
||||
const bi = Number($(this).data('block'));
|
||||
if (editorBlocks[bi]) {
|
||||
editorBlocks[bi].chat = $(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);
|
||||
if (editorBlocks[bi].bullets.length === 0) {
|
||||
editorBlocks.splice(bi, 1);
|
||||
reindexEditingSet(editingSet, bi);
|
||||
}
|
||||
refreshEditor();
|
||||
}
|
||||
});
|
||||
|
||||
// Delete block
|
||||
$(document).off('click.charMemoryEditorDelBlock').on('click.charMemoryEditorDelBlock', '.charMemory_editorDeleteBlock', function () {
|
||||
const bi = Number($(this).data('block'));
|
||||
editorBlocks.splice(bi, 1);
|
||||
reindexEditingSet(editingSet, bi);
|
||||
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();
|
||||
$(`#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')}`;
|
||||
const newIdx = editorBlocks.length;
|
||||
editorBlocks.push({ chat: 'New Group', date: timestamp, bullets: [''] });
|
||||
editingSet.add(newIdx);
|
||||
refreshEditor();
|
||||
$('#charMemory_editorPane .charMemory_editorCard:last .charMemory_editorBulletInput:last').focus();
|
||||
});
|
||||
|
||||
// === Dialog prompt handlers ===
|
||||
$('#charMemory_dialogPrompt').off('input').on('input', function () {
|
||||
const strategy = $('#charMemory_consolidationDialogStrategy').val();
|
||||
if (!extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
extension_settings[MODULE_NAME].consolidationPrompts = {};
|
||||
}
|
||||
extension_settings[MODULE_NAME].consolidationPrompts[strategy] = $(this).val();
|
||||
$('#charMemory_dialogRestoreDefault').show();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#charMemory_dialogRestoreDefault').off('click').on('click', function () {
|
||||
const strategy = $('#charMemory_consolidationDialogStrategy').val();
|
||||
if (extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
delete extension_settings[MODULE_NAME].consolidationPrompts[strategy];
|
||||
}
|
||||
const preset = CONSOLIDATION_PRESETS[strategy];
|
||||
$('#charMemory_dialogPrompt').val(preset?.prompt || '');
|
||||
$('#charMemory_dialogRestoreDefault').hide();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#charMemory_consolidationDialogStrategy').off('change').on('change', function () {
|
||||
const strategy = $(this).val();
|
||||
const dlgOverrides = extension_settings[MODULE_NAME].consolidationPrompts || {};
|
||||
const prompt = dlgOverrides[strategy] || CONSOLIDATION_PRESETS[strategy]?.prompt || '';
|
||||
const isCustomized = !!dlgOverrides[strategy];
|
||||
$('#charMemory_dialogPrompt').val(prompt);
|
||||
$('#charMemory_dialogRestoreDefault').toggle(isCustomized);
|
||||
});
|
||||
|
||||
// === 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);
|
||||
$('#charMemory_editorPane').addClass('charMemory_editorDisabled');
|
||||
|
||||
const newResult = await runConsolidationLLM(memories);
|
||||
|
||||
$('#charMemory_rerunSpinner').hide();
|
||||
$('#charMemory_rerunConsolidation').prop('disabled', false);
|
||||
$('#charMemory_editorPane').removeClass('charMemory_editorDisabled');
|
||||
|
||||
if (newResult) {
|
||||
versionStack.push(currentBlocks);
|
||||
$('#charMemory_undoRerun').prop('disabled', false);
|
||||
editorBlocks = parseMemories(newResult);
|
||||
editingSet.clear();
|
||||
refreshEditor();
|
||||
}
|
||||
});
|
||||
|
||||
// === Undo button ===
|
||||
$('#charMemory_undoRerun').off('click').on('click', () => {
|
||||
if (versionStack.length === 0) return;
|
||||
editorBlocks = versionStack.pop();
|
||||
editingSet.clear();
|
||||
refreshEditor();
|
||||
if (versionStack.length === 0) {
|
||||
$('#charMemory_undoRerun').prop('disabled', true);
|
||||
}
|
||||
});
|
||||
|
||||
// === Wait for Accept/Cancel ===
|
||||
const confirmed = await popup;
|
||||
|
||||
// Clean up event delegation
|
||||
$(document).off('click.charMemoryEditorToggle');
|
||||
$(document).off('input.charMemoryEditor');
|
||||
$(document).off('input.charMemoryEditorTheme');
|
||||
$(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');
|
||||
updateConsolidationStrategyUI();
|
||||
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();
|
||||
updateConsolidationStrategyUI();
|
||||
}
|
||||
|
||||
// ============ Slash Commands ============
|
||||
|
||||
function registerSlashCommands() {
|
||||
|
|
@ -3370,6 +3761,33 @@ function setupListeners() {
|
|||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#charMemory_consolidationStrategy').off('change').on('change', function () {
|
||||
extension_settings[MODULE_NAME].consolidationStrategy = String($(this).val());
|
||||
updateConsolidationStrategyUI();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
// Consolidation prompt editing — save override for current strategy
|
||||
$('#charMemory_consolidationPrompt').off('input').on('input', function () {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
if (!extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
extension_settings[MODULE_NAME].consolidationPrompts = {};
|
||||
}
|
||||
extension_settings[MODULE_NAME].consolidationPrompts[strategy] = $(this).val();
|
||||
$('#charMemory_restorePresetDefault').show();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
// Restore preset default prompt
|
||||
$('#charMemory_restorePresetDefault').off('click').on('click', function () {
|
||||
const strategy = extension_settings[MODULE_NAME].consolidationStrategy || 'balanced';
|
||||
if (extension_settings[MODULE_NAME].consolidationPrompts) {
|
||||
delete extension_settings[MODULE_NAME].consolidationPrompts[strategy];
|
||||
}
|
||||
updateConsolidationStrategyUI();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#charMemory_extractNow').off('click').on('click', function () {
|
||||
extractMemories({ force: true });
|
||||
});
|
||||
|
|
@ -3433,6 +3851,11 @@ function setupListeners() {
|
|||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#charMemory_mergeChunks').off('change').on('change', function () {
|
||||
extension_settings[MODULE_NAME].mergeChunks = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#charMemory_perChat').off('change').on('change', function () {
|
||||
extension_settings[MODULE_NAME].perChat = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
|
|
@ -3443,7 +3866,7 @@ function setupListeners() {
|
|||
$('#charMemory_consolidate').off('click').on('click', () => consolidateMemories());
|
||||
$('#charMemory_undoConsolidate').off('click').on('click', () => undoConsolidation());
|
||||
|
||||
// Tab switching for Activity, Diagnostics & Batch panels
|
||||
// Tab switching for top-level panel tabs
|
||||
$('.charMemory_tab').off('click').on('click', function () {
|
||||
const tab = $(this).data('tab');
|
||||
$('.charMemory_tab').removeClass('active');
|
||||
|
|
@ -3479,6 +3902,8 @@ function setupListeners() {
|
|||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Batch Extract tab
|
||||
$('#charMemory_batchRefresh').off('click').on('click', loadBatchChatList);
|
||||
$('#charMemory_batchExtract').off('click').on('click', runBatchExtraction);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "bal-spec",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"homePage": "",
|
||||
"auto_update": false
|
||||
}
|
||||
|
|
|
|||
544
settings.html
544
settings.html
|
|
@ -16,291 +16,329 @@
|
|||
<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. When this fills up, extraction triggers automatically.">
|
||||
<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. Manual extractions bypass this.">
|
||||
<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>
|
||||
<!-- 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 Extraction</button>
|
||||
<button class="charMemory_tab" data-tab="settings">Settings</button>
|
||||
<button class="charMemory_tab" data-tab="log">Log</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- Settings (default closed) -->
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Settings</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
<!-- Main tab (default) -->
|
||||
<div class="charMemory_tabContent" id="charMemory_tabMain">
|
||||
<div class="charMemory_sectionHeader">
|
||||
<small><b>Memory Extraction</b></small>
|
||||
<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" />
|
||||
<small>Automatic</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Section 1: 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. Dedicated API sends a clean, focused extraction prompt directly to the LLM.</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>
|
||||
<!-- 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>
|
||||
</select>
|
||||
<small id="charMemory_consolidationPreview" class="charMemory_helperText" style="font-style:italic;"></small>
|
||||
<details class="charMemory_promptDisclosure" id="charMemory_promptDisclosure">
|
||||
<summary><small>Show prompt</small></summary>
|
||||
<textarea id="charMemory_consolidationPrompt" class="text_pole textarea_compact" rows="6" placeholder="Edit the consolidation prompt for this strategy..."></textarea>
|
||||
<div class="charMemory_buttonRow">
|
||||
<input type="button" id="charMemory_restorePresetDefault" class="menu_button" value="Restore Default" title="Reset this preset's prompt to its built-in default" style="display:none;" />
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</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>
|
||||
|
||||
<!-- Section 2: Auto-Extraction -->
|
||||
<hr class="charMemory_separator" />
|
||||
<small><b>Auto-Extraction</b></small>
|
||||
<!-- 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 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 class="charMemory_sectionHeader">
|
||||
<small><b title="Chat files attached to this character. Select which ones to extract memories from.">Character Attachments</b></small>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="charMemory_batchSelectAll" />
|
||||
<small>Select all</small>
|
||||
</label>
|
||||
</div>
|
||||
<div id="charMemory_batchChatList" class="charMemory_batchChatList">
|
||||
<div class="charMemory_diagEmpty">Click "Refresh" to load chats.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charMemory_sliderRow">
|
||||
<label title="How many new messages trigger an automatic extraction. Only counts messages received after the last 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>
|
||||
<!-- Settings tab -->
|
||||
<div class="charMemory_tabContent" id="charMemory_tabSettings" style="display:none;">
|
||||
|
||||
<div class="charMemory_sliderRow">
|
||||
<label title="Minimum time between auto-extractions, even if the message threshold is met. Prevents rapid-fire extractions. Manual 'Extract Now' bypasses this.">
|
||||
<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>
|
||||
|
||||
<!-- Section 3: 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. The system loops through all unprocessed messages in chunks of this size.">
|
||||
<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. Increase if extractions seem truncated.">
|
||||
<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>
|
||||
|
||||
<div class="charMemory_statusRow">
|
||||
<label class="checkbox_label" for="charMemory_perChat" title="When enabled, each chat gets its own memory file. When disabled, all chats for a character share one file. Applies to both 1:1 and group chats.">
|
||||
<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. Applies to both 1:1 and group chats.</small>
|
||||
</div>
|
||||
|
||||
<!-- Section 4: 1:1 Chat (hidden in group chats) -->
|
||||
<div id="charMemory_section1v1">
|
||||
<hr class="charMemory_separator" />
|
||||
<small><b>1:1 Chat</b></small>
|
||||
<!-- 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 for="charMemory_fileName" title="Override the auto-generated file name. Leave blank to use the default (based on character 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">Current file: <span id="charMemory_resolvedFileName">—</span></small>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Section 5: Group Chat (hidden in 1:1 chats) -->
|
||||
<div id="charMemory_sectionGroup" style="display:none;">
|
||||
<hr class="charMemory_separator" />
|
||||
<small><b>Group Chat</b></small>
|
||||
|
||||
<div class="charMemory_statusRow">
|
||||
<label for="charMemory_groupMode" title="How to extract memories from group chats. Per-character sends one LLM call per character per chunk.">
|
||||
<small>Extraction mode</small>
|
||||
</label>
|
||||
<select id="charMemory_groupMode" class="text_pole">
|
||||
<option value="per-character">Per-character (one LLM call per character)</option>
|
||||
<label><small>Provider</small></label>
|
||||
<select id="charMemory_providerSelect" class="text_pole">
|
||||
</select>
|
||||
<small class="charMemory_helperText">Each character gets a focused extraction with their own card and existing memories.</small>
|
||||
</div>
|
||||
|
||||
<div id="charMemory_groupMembersSection">
|
||||
<label><small>Member memory files</small></label>
|
||||
<div id="charMemory_groupMembersList" class="charMemory_groupMembersList">
|
||||
<small class="charMemory_helperText">Open a group chat to see members.</small>
|
||||
<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>
|
||||
<small class="charMemory_helperText">Each character's memories are stored in their own Data Bank. Leave blank for auto-naming. If a character already has a memory file, it will be detected automatically.</small>
|
||||
</div>
|
||||
|
||||
<div class="charMemory_promptSection">
|
||||
<label for="charMemory_groupExtractionPrompt" title="The prompt sent to the LLM for group chat memory extraction. Uses {{charName}}, {{charCard}}, {{participants}}, {{existingMemories}}, {{recentMessages}}, {{char}}, and {{user}} placeholders.">
|
||||
<small>Extraction prompt</small>
|
||||
</label>
|
||||
<textarea id="charMemory_groupExtractionPrompt" class="text_pole textarea_compact" rows="8" placeholder="Enter group extraction prompt..."></textarea>
|
||||
<input type="button" id="charMemory_restoreGroupPrompt" class="menu_button" value="Restore Default Prompt" title="Replace the group prompt with the built-in default" />
|
||||
<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>
|
||||
|
||||
<div class="charMemory_statusRow">
|
||||
<label class="checkbox_label" for="charMemory_mergeChunks" title="When enabled, extraction results from the same chat are merged into a single block. Disable for long chats to keep blocks smaller for consolidation.">
|
||||
<input type="checkbox" id="charMemory_mergeChunks" />
|
||||
<span>Merge extraction chunks</span>
|
||||
</label>
|
||||
<small class="charMemory_helperText">When enabled, multi-chunk extractions from the same chat are merged into one block. Disable for long chats to keep blocks smaller for consolidation.</small>
|
||||
</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. When disabled, all chats for a character share one file. Applies to both 1:1 and group chats.">
|
||||
<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. Applies to both 1:1 and group chats.</small>
|
||||
</div>
|
||||
|
||||
<!-- 1:1 Chat section (hidden in group chats) -->
|
||||
<div id="charMemory_section1v1">
|
||||
<div class="charMemory_statusRow">
|
||||
<label for="charMemory_fileName" title="Override the auto-generated file name. Leave blank to use the default (based on character 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">Current file: <span id="charMemory_resolvedFileName">—</span></small>
|
||||
</div>
|
||||
|
||||
<!-- Section 6: Advanced -->
|
||||
<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 so all messages can be re-processed. Does not delete any memories." />
|
||||
<small class="charMemory_helperText">Resets extraction tracking for the current character (active chat + batch state). Use before 'Extract Now' or 'Batch Extract' to re-process from the beginning. Does not affect other characters.</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. This cannot be undone." />
|
||||
<small class="charMemory_helperText">Deletes the memory file and resets extraction tracking for the current character. In default mode, this file contains memories from all of this character's chats. This cannot be undone.</small>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Group Chat section (hidden in 1:1 chats) -->
|
||||
<div id="charMemory_sectionGroup" style="display:none;">
|
||||
<div class="charMemory_statusRow">
|
||||
<label for="charMemory_groupMode" title="How to extract memories from group chats. Per-character sends one LLM call per character per chunk.">
|
||||
<small>Extraction mode</small>
|
||||
</label>
|
||||
<select id="charMemory_groupMode" class="text_pole">
|
||||
<option value="per-character">Per-character (one LLM call per character)</option>
|
||||
</select>
|
||||
<small class="charMemory_helperText">Each character gets a focused extraction with their own card and existing memories.</small>
|
||||
</div>
|
||||
|
||||
<div id="charMemory_groupMembersSection">
|
||||
<label><small>Member memory files</small></label>
|
||||
<div id="charMemory_groupMembersList" class="charMemory_groupMembersList">
|
||||
<small class="charMemory_helperText">Open a group chat to see members.</small>
|
||||
</div>
|
||||
<small class="charMemory_helperText">Each character's memories are stored in their own Data Bank. Leave blank for auto-naming. If a character already has a memory file, it will be detected automatically.</small>
|
||||
</div>
|
||||
|
||||
<hr class="charMemory_separator" />
|
||||
|
||||
<div class="charMemory_promptSection">
|
||||
<label for="charMemory_groupExtractionPrompt" title="The prompt sent to the LLM for group chat memory extraction. Uses {{charName}}, {{charCard}}, {{participants}}, {{existingMemories}}, {{recentMessages}}, {{char}}, and {{user}} placeholders.">
|
||||
<small>Extraction prompt</small>
|
||||
</label>
|
||||
<textarea id="charMemory_groupExtractionPrompt" class="text_pole textarea_compact" rows="8" placeholder="Enter group extraction prompt..."></textarea>
|
||||
<input type="button" id="charMemory_restoreGroupPrompt" class="menu_button" value="Restore Default Prompt" title="Replace the group prompt with the built-in default" />
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Tools & Diagnostics (tabbed, default closed) -->
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Tools & Diagnostics</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></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 class="inline-drawer-content">
|
||||
<div class="charMemory_tabs">
|
||||
<button class="charMemory_tab" data-tab="batch">Batch Extract</button>
|
||||
<button class="charMemory_tab active" data-tab="log">Activity Log</button>
|
||||
<button class="charMemory_tab" data-tab="diag">Diagnostics</button>
|
||||
</div>
|
||||
<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>
|
||||
<div class="charMemory_tabContent" id="charMemory_tabLog">
|
||||
<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 class="charMemory_tabContent" id="charMemory_tabDiag" style="display:none;">
|
||||
<div class="charMemory_buttonRow">
|
||||
<input type="button" id="charMemory_refreshDiag" class="menu_button" value="Refresh" 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" after a generation to see diagnostics.</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Activity Log — always visible -->
|
||||
<div class="charMemory_miniLog" id="charMemory_miniLog">
|
||||
<small><b>Activity Log</b></small>
|
||||
<div class="charMemory_miniLogContent" id="charMemory_miniLogContent">
|
||||
<div class="charMemory_diagEmpty charMemory_miniLogEmpty">No activity yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagnostics — always visible at bottom -->
|
||||
<div class="charMemory_bottomDiagnostics">
|
||||
<div class="charMemory_buttonRow">
|
||||
<small><b>Diagnostics</b></small>
|
||||
<input type="button" id="charMemory_refreshDiag" class="menu_button" value="Refresh" 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" after a generation to see diagnostics.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
223
style.css
223
style.css
|
|
@ -46,8 +46,8 @@
|
|||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.85em;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
min-width: 80px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -66,13 +66,14 @@
|
|||
/* Tabs */
|
||||
.charMemory_tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
margin-bottom: 6px;
|
||||
border-bottom: 1px solid var(--SmartThemeBorderColor, rgba(128, 128, 128, 0.2));
|
||||
}
|
||||
|
||||
.charMemory_tab {
|
||||
padding: 6px 14px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.85em;
|
||||
background: none;
|
||||
border: none;
|
||||
|
|
@ -81,6 +82,7 @@
|
|||
opacity: 0.6;
|
||||
color: var(--SmartThemeBodyColor, #ccc);
|
||||
transition: opacity 0.15s, border-color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.charMemory_tab:hover {
|
||||
|
|
@ -101,6 +103,14 @@
|
|||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Section header — label + control on same line */
|
||||
.charMemory_sectionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Separator */
|
||||
.charMemory_separator {
|
||||
border: none;
|
||||
|
|
@ -117,6 +127,8 @@
|
|||
.charMemory_diagnosticsContent {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.charMemory_diagTimestamp {
|
||||
|
|
@ -208,6 +220,7 @@
|
|||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.charMemory_card {
|
||||
|
|
@ -216,6 +229,13 @@
|
|||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.charMemory_card ul {
|
||||
margin: 0;
|
||||
padding-left: 1.2em;
|
||||
font-size: 0.85em;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.charMemory_cardHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -389,3 +409,200 @@
|
|||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
/* Consolidation Dialog */
|
||||
.charMemory_consolidationDialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.charMemory_consolidationStats {
|
||||
font-size: 0.9em;
|
||||
padding: 6px 10px;
|
||||
background: var(--SmartThemeBlurTintColor, rgba(0, 0, 0, 0.1));
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.charMemory_consolidationPanes {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.charMemory_consolidationPane {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.charMemory_consolidationPane h4 {
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.charMemory_consolidationContent {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.charMemory_consolidationToolbar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--SmartThemeBorderColor, rgba(255,255,255,0.1));
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.charMemory_editorCard--editing {
|
||||
border: 1px solid var(--SmartThemeBorderColor, rgba(255,255,255,0.15));
|
||||
}
|
||||
|
||||
.charMemory_editorToggleEdit {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.charMemory_editorToggleEdit:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.charMemory_editorThemeInput {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 2px 6px;
|
||||
font-weight: bold;
|
||||
font-size: 0.95em;
|
||||
background: var(--SmartThemeBlurTintColor, rgba(0, 0, 0, 0.05));
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.charMemory_editorThemeInput:focus {
|
||||
border-color: var(--SmartThemeBorderColor, rgba(255,255,255,0.2));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.charMemory_editorAddBlock--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.charMemory_promptDisclosure {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.charMemory_promptDisclosure summary {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.charMemory_promptDisclosure summary:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.charMemory_promptDisclosure textarea {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.charMemory_editorDisabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Activity Log — always visible */
|
||||
.charMemory_miniLog {
|
||||
border-top: 1px solid var(--SmartThemeBorderColor, rgba(255,255,255,0.1));
|
||||
margin-top: 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.charMemory_miniLogContent {
|
||||
font-size: 0.8em;
|
||||
font-family: monospace;
|
||||
height: 60px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
resize: vertical;
|
||||
min-height: 30px;
|
||||
max-height: 400px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.charMemory_miniLogEmpty {
|
||||
padding: 2px 0;
|
||||
font-size: 0.9em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Bottom Diagnostics — always visible */
|
||||
.charMemory_bottomDiagnostics {
|
||||
border-top: 1px solid var(--SmartThemeBorderColor, rgba(255,255,255,0.1));
|
||||
margin-top: 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.charMemory_bottomDiagnostics .charMemory_buttonRow {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.charMemory_bottomDiagnostics .charMemory_diagnosticsContent {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue