sillytavern-character-memory/editor.js
bal-spec dc0eab2638 v2.1.6 — UX redesign, injection viewer, unified editor, token breakdown
Complete rewrite of the UI and significant feature additions since v1.6.1.

UX Redesign (v2.0):
- Single-view dashboard replaces 4-tab sidebar
- Settings, Prompts, Troubleshooter, Memory Manager moved to center-screen modals
- Activity log in slide-out drawer
- Setup Wizard for first-run configuration
- Prompt version tracking with update notifications
- Health indicator in stats bar

Injection Viewer (v1.6–v2.1.6):
- Per-message injection data: see exactly what memories, lorebook entries,
  and extension prompts were injected for any generation
- Context/Prompt Breakdown with per-category token counts (System, Char card,
  Lorebook, Data Bank, Examples, Chat history) via ST Prompt Itemization
- Stacked bar visualization, token hints in headers, Tips popup
- Context overflow and heavy injection warnings

Memory Management:
- Unified block editor across all 5 editing surfaces (Memory Manager,
  Consolidation, Conversion, Reformat, Data Bank browser)
- Find & Replace with highlighting across all editors
- Undo support for all edit operations
- Group chat character picker in Memory Manager

Other features:
- Tablet & phone display modes with touch-friendly controls
- Topic-tagged memory format for better vector retrieval
- Self-closing memory tag handling (GLM-4.7 compatibility)
- Protect recent messages from extraction feedback loop
- 9-point health check system with retrieve chunks and score threshold
- Shared editor factory (editor.js), pure utility library (lib.js)
- Vitest test suite: unit, snapshot, and live LLM tests
- Full documentation suite in docs/

See CHANGELOG.md for detailed per-version notes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:20:14 -08:00

110 lines
3.3 KiB
JavaScript

import { cloneMemoryBlocks, countMatchesInBlocks, getTimestamp, reindexEditingSet, replaceInBlocks } from './lib.js';
/**
* Create a memory block editor with state management and undo.
* Pure state logic — no DOM. Callers handle rendering and event binding.
*
* @param {object} options
* @param {Array<{chat: string, date: string, bullets: string[]}>} options.blocks - Initial blocks
* @returns {object} Editor API
*/
export function createMemoryEditor({ blocks }) {
let editorBlocks = cloneMemoryBlocks(blocks);
const versionStack = [];
const editingSet = new Set();
function saveVersion() {
versionStack.push(cloneMemoryBlocks(editorBlocks));
}
return {
getBlocks() {
return cloneMemoryBlocks(editorBlocks);
},
deleteBullet(blockIndex, bulletIndex) {
if (!editorBlocks[blockIndex]) return;
saveVersion();
editorBlocks[blockIndex].bullets.splice(bulletIndex, 1);
if (editorBlocks[blockIndex].bullets.length === 0) {
editorBlocks.splice(blockIndex, 1);
reindexEditingSet(editingSet, blockIndex);
}
},
deleteBlock(blockIndex) {
if (!editorBlocks[blockIndex]) return;
saveVersion();
editorBlocks.splice(blockIndex, 1);
reindexEditingSet(editingSet, blockIndex);
},
addBullet(blockIndex) {
if (!editorBlocks[blockIndex]) return;
saveVersion();
editorBlocks[blockIndex].bullets.push('');
editingSet.add(blockIndex);
},
addBlock(timestamp) {
saveVersion();
editorBlocks.push({
chat: 'New Group',
date: timestamp || getTimestamp(),
bullets: [''],
});
editingSet.add(editorBlocks.length - 1);
},
updateBullet(blockIndex, bulletIndex, text) {
if (!editorBlocks[blockIndex]) return;
editorBlocks[blockIndex].bullets[bulletIndex] = text;
},
updateTheme(blockIndex, label) {
if (!editorBlocks[blockIndex]) return;
editorBlocks[blockIndex].chat = label;
},
undo() {
if (versionStack.length === 0) return false;
editorBlocks = versionStack.pop();
return true;
},
canUndo() {
return versionStack.length > 0;
},
replaceAll(newBlocks) {
editorBlocks = cloneMemoryBlocks(newBlocks);
versionStack.length = 0;
editingSet.clear();
},
toggleEdit(blockIndex) {
if (editingSet.has(blockIndex)) {
editingSet.delete(blockIndex);
} else {
editingSet.add(blockIndex);
}
},
isEditing(blockIndex) {
return editingSet.has(blockIndex);
},
getEditingSet() {
return new Set(editingSet);
},
countMatches(find, caseSensitive = false) {
return countMatchesInBlocks(editorBlocks, find, caseSensitive);
},
findAndReplaceAll(find, replace, caseSensitive = false) {
saveVersion();
return { replacements: replaceInBlocks(editorBlocks, find, replace, caseSensitive) };
},
};
}