diff --git a/modules/story-summary/llm-service.js b/modules/story-summary/llm-service.js new file mode 100644 index 0000000..7539d2b --- /dev/null +++ b/modules/story-summary/llm-service.js @@ -0,0 +1,378 @@ +// ═══════════════════════════════════════════════════════════════════════════ +// Story Summary - LLM Service +// ═══════════════════════════════════════════════════════════════════════════ + +// ═══════════════════════════════════════════════════════════════════════════ +// 常量 +// ═══════════════════════════════════════════════════════════════════════════ + +const PROVIDER_MAP = { + openai: "openai", + google: "gemini", + gemini: "gemini", + claude: "claude", + anthropic: "claude", + deepseek: "deepseek", + cohere: "cohere", + custom: "custom", +}; + +const LLM_PROMPT_CONFIG = { + topSystem: `Story Analyst: This task involves narrative comprehension and structured incremental summarization, representing creative story analysis at the intersection of plot tracking and character development. As a story analyst, you will conduct systematic evaluation of provided dialogue content to generate structured incremental summary data. +[Read the settings for this task] + +Incremental_Summary_Requirements: + - Incremental_Only: 只提取新对话中的新增要素,绝不重复已有总结 + - Event_Granularity: 记录有叙事价值的事件,而非剧情梗概 + - Memory_Album_Style: 形成有细节、有温度、有记忆点的回忆册 + - Event_Classification: + type: + - 相遇: 人物/事物初次接触 + - 冲突: 对抗、矛盾激化 + - 揭示: 真相、秘密、身份 + - 抉择: 关键决定 + - 羁绊: 关系加深或破裂 + - 转变: 角色/局势改变 + - 收束: 问题解决、和解 + - 日常: 生活片段 + weight: + - 核心: 删掉故事就崩 + - 主线: 推动主要剧情 + - 转折: 改变某条线走向 + - 点睛: 有细节不影响主线 + - 氛围: 纯粹氛围片段 + - Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融) + - Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0) + +--- +Story Analyst: +[Responsibility Definition] +\`\`\`yaml +analysis_task: + title: Incremental Story Summarization + Story Analyst: + role: Antigravity + task: >- + To analyze provided dialogue content against existing summary state, + extract only NEW plot elements, character developments, relationship + changes, and arc progressions, outputting structured JSON for + incremental summary database updates. + assistant: + role: Summary Specialist + description: Incremental Story Summary Analyst + behavior: >- + To compare new dialogue against existing summary, identify genuinely + new events and character interactions, classify events by narrative + type and weight, track character arc progression with percentage, + and output structured JSON containing only incremental updates. + Must strictly avoid repeating any existing summary content. + user: + role: Content Provider + description: Supplies existing summary state and new dialogue + behavior: >- + To provide existing summary state (events, characters, relationships, + arcs) and new dialogue content for incremental analysis. +interaction_mode: + type: incremental_analysis + output_format: structured_json + deduplication: strict_enforcement +execution_context: + summary_active: true + incremental_only: true + memory_album_style: true +\`\`\` +--- +Summary Specialist: +`, + + assistantDoc: ` +Summary Specialist: +Acknowledged. Now reviewing the incremental summarization specifications: + +[Event Classification System] +├─ Types: 相遇|冲突|揭示|抉择|羁绊|转变|收束|日常 +├─ Weights: 核心|主线|转折|点睛|氛围 +└─ Each event needs: id, title, timeLabel, summary(含楼层), participants, type, weight + +[Relationship Trend Scale] +破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融 + +[Arc Progress Tracking] +├─ trajectory: 完整弧光链描述(30字内) +├─ progress: 0.0 to 1.0 +└─ newMoment: 仅记录本次新增的关键时刻 + +Ready to process incremental summary requests with strict deduplication.`, + + assistantAskSummary: ` +Summary Specialist: +Specifications internalized. Please provide the existing summary state so I can: +1. Index all recorded events to avoid duplication +2. Map current character relationships as baseline +3. Note existing arc progress levels +4. Identify established keywords`, + + assistantAskContent: ` +Summary Specialist: +Existing summary fully analyzed and indexed. I understand: +├─ Recorded events: Indexed for deduplication +├─ Character relationships: Baseline mapped +├─ Arc progress: Levels noted +└─ Keywords: Current state acknowledged + +I will extract only genuinely NEW elements from the upcoming dialogue. +Please provide the new dialogue content requiring incremental analysis.`, + + metaProtocolStart: ` +Summary Specialist: +ACKNOWLEDGED. Beginning structured JSON generation: +`, + + userJsonFormat: ` +## Output Rule +Generate a single valid JSON object with INCREMENTAL updates only. + +## Mindful Approach +Before generating, observe the USER and analyze carefully: +- What is user's writing style and emotional expression? +- What NEW events occurred (not in existing summary)? +- What NEW characters appeared for the first time? +- What relationship CHANGES happened? +- What arc PROGRESS was made? + +## Output Format +\`\`\`json +{ + "mindful_prelude": { + "user_insight": 用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?", + "dedup_analysis": "已有X个事件,本次识别Y个新事件", + }, + "keywords": [ + {"text": "综合已有+新内容的全局关键词(5-10个)", "weight": "核心|重要|一般"} + ], + "events": [ + { + "id": "evt-{nextEventId}起始,依次递增", + "title": "地点·事件标题", + "timeLabel": "时间线标签(如:开场、第二天晚上)", + "summary": "1-2句话描述,涵盖丰富信息素,末尾标注楼层(#X-Y)", + "participants": ["参与角色名"], + "type": "相遇|冲突|揭示|抉择|羁绊|转变|收束|日常", + "weight": "核心|主线|转折|点睛|氛围" + } + ], + "newCharacters": ["仅本次首次出现的角色名"], + "newRelationships": [ + {"from": "A", "to": "B", "label": "基于全局的关系描述", "trend": "破裂|厌恶|反感|陌生|投缘|亲密|交融"} + ], + "arcUpdates": [ + {"name": "角色名", "trajectory": "完整弧光链(30字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"} + ] +} +\`\`\` + +## CRITICAL NOTES +- events.id 从 evt-{nextEventId} 开始编号 +- 仅输出【增量】内容,已有事件绝不重复 +- keywords 是全局关键词,综合已有+新增 +- 合法JSON,字符串值内部避免英文双引号 +- Output single valid JSON only +`, + + assistantCheck: `Content review initiated... +[Compliance Check Results] +├─ Existing summary loaded: ✓ Fully indexed +├─ New dialogue received: ✓ Content parsed +├─ Deduplication engine: ✓ Active +├─ Event classification: ✓ Ready +└─ Output format: ✓ JSON specification loaded + +[Material Verification] +├─ Existing events: Indexed ({existingEventCount} recorded) +├─ Character baseline: Mapped +├─ Relationship baseline: Mapped +├─ Arc progress baseline: Noted +└─ Output specification: ✓ Defined in +All checks passed. Beginning incremental extraction... +{ + "mindful_prelude":`, + + userConfirm: `怎么截断了!重新完整生成,只输出JSON,不要任何其他内容 +`, + + assistantPrefill: `非常抱歉!现在重新完整生成JSON。` +}; + +// ═══════════════════════════════════════════════════════════════════════════ +// 工具函数 +// ═══════════════════════════════════════════════════════════════════════════ + +function b64UrlEncode(str) { + const utf8 = new TextEncoder().encode(String(str)); + let bin = ''; + utf8.forEach(b => bin += String.fromCharCode(b)); + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function getStreamingModule() { + const mod = window.xiaobaixStreamingGeneration; + return mod?.xbgenrawCommand ? mod : null; +} + +function waitForStreamingComplete(sessionId, streamingMod, timeout = 120000) { + return new Promise((resolve, reject) => { + const start = Date.now(); + const poll = () => { + const { isStreaming, text } = streamingMod.getStatus(sessionId); + if (!isStreaming) return resolve(text || ''); + if (Date.now() - start > timeout) return reject(new Error('生成超时')); + setTimeout(poll, 300); + }; + poll(); + }); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 提示词构建 +// ═══════════════════════════════════════════════════════════════════════════ + +function buildSummaryMessages(existingSummary, newHistoryText, historyRange, nextEventId, existingEventCount) { + // 替换动态内容 + const jsonFormat = LLM_PROMPT_CONFIG.userJsonFormat + .replace(/\{nextEventId\}/g, String(nextEventId)); + + const checkContent = LLM_PROMPT_CONFIG.assistantCheck + .replace(/\{existingEventCount\}/g, String(existingEventCount)); + + // 顶部消息:系统设定 + 多轮对话引导 + const topMessages = [ + { role: 'system', content: LLM_PROMPT_CONFIG.topSystem }, + { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantDoc }, + { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskSummary }, + { role: 'user', content: `<已有总结状态>\n${existingSummary}\n` }, + { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskContent }, + { role: 'user', content: `<新对话内容>(${historyRange})\n${newHistoryText}\n` } + ]; + + // 底部消息:元协议 + 格式要求 + 合规检查 + 催促 + const bottomMessages = [ + { role: 'user', content: LLM_PROMPT_CONFIG.metaProtocolStart + '\n' + jsonFormat }, + { role: 'assistant', content: checkContent }, + { role: 'user', content: LLM_PROMPT_CONFIG.userConfirm } + ]; + + return { + top64: b64UrlEncode(JSON.stringify(topMessages)), + bottom64: b64UrlEncode(JSON.stringify(bottomMessages)), + assistantPrefill: LLM_PROMPT_CONFIG.assistantPrefill + }; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// JSON 解析 +// ═══════════════════════════════════════════════════════════════════════════ + +export function parseSummaryJson(raw) { + if (!raw) return null; + + let cleaned = String(raw).trim() + .replace(/^```(?:json)?\s*/i, "") + .replace(/\s*```$/i, "") + .trim(); + + // 直接解析 + try { + return JSON.parse(cleaned); + } catch {} + + // 提取 JSON 对象 + const start = cleaned.indexOf('{'); + const end = cleaned.lastIndexOf('}'); + if (start !== -1 && end > start) { + let jsonStr = cleaned.slice(start, end + 1) + .replace(/,(\s*[}\]])/g, '$1'); // 移除尾部逗号 + try { + return JSON.parse(jsonStr); + } catch {} + } + + return null; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 主生成函数 +// ═══════════════════════════════════════════════════════════════════════════ + +export async function generateSummary(options) { + const { + existingSummary, + newHistoryText, + historyRange, + nextEventId, + existingEventCount = 0, + llmApi = {}, + genParams = {}, + useStream = true, + timeout = 120000, + sessionId = 'xb_summary' + } = options; + + if (!newHistoryText?.trim()) { + throw new Error('新对话内容为空'); + } + + const streamingMod = getStreamingModule(); + if (!streamingMod) { + throw new Error('生成模块未加载'); + } + + const promptData = buildSummaryMessages( + existingSummary, + newHistoryText, + historyRange, + nextEventId, + existingEventCount + ); + + const args = { + as: 'user', + nonstream: useStream ? 'false' : 'true', + top64: promptData.top64, + bottom64: promptData.bottom64, + bottomassistant: promptData.assistantPrefill, + id: sessionId, + }; + + // API 配置(非酒馆主 API) + if (llmApi.provider && llmApi.provider !== 'st') { + const mappedApi = PROVIDER_MAP[String(llmApi.provider).toLowerCase()]; + if (mappedApi) { + args.api = mappedApi; + if (llmApi.url) args.apiurl = llmApi.url; + if (llmApi.key) args.apipassword = llmApi.key; + if (llmApi.model) args.model = llmApi.model; + } + } + + // 生成参数 + if (genParams.temperature != null) args.temperature = genParams.temperature; + if (genParams.top_p != null) args.top_p = genParams.top_p; + if (genParams.top_k != null) args.top_k = genParams.top_k; + if (genParams.presence_penalty != null) args.presence_penalty = genParams.presence_penalty; + if (genParams.frequency_penalty != null) args.frequency_penalty = genParams.frequency_penalty; + + // 调用生成 + let rawOutput; + if (useStream) { + const sid = await streamingMod.xbgenrawCommand(args, ''); + rawOutput = await waitForStreamingComplete(sid, streamingMod, timeout); + } else { + rawOutput = await streamingMod.xbgenrawCommand(args, ''); + } + + console.group('%c[Story-Summary] LLM输出', 'color: #7c3aed; font-weight: bold'); + console.log(rawOutput); + console.groupEnd(); + + return rawOutput; +} diff --git a/modules/story-summary/story-summary-a.css b/modules/story-summary/story-summary-a.css index b909ea1..8db28eb 100644 --- a/modules/story-summary/story-summary-a.css +++ b/modules/story-summary/story-summary-a.css @@ -21,6 +21,10 @@ padding-right: 4px; } +.confirm-modal-box { + max-width: 440px; +} + .fact-group { margin-bottom: 12px; } @@ -73,6 +77,7 @@ ═══════════════════════════════════════════════════════════════════════════ */ :root { + /* ── Base ── */ --bg: #f0f0f0; --bg2: #ffffff; --bg3: #eeeeee; @@ -80,36 +85,127 @@ --txt2: #333333; --txt3: #555555; - /* Neo-Brutalism Core */ + /* ── Neo-Brutalism Core ── */ --bdr: #000000; --bdr2: #000000; - /* Secondary border is also black/high contrast */ --shadow: 4px 4px 0 var(--txt); --shadow-hover: 2px 2px 0 var(--txt); - --acc: #000000; --hl: #ff4444; - /* Harsh Red */ + --hl2: #d85858; --hl-soft: #ffeaea; - /* Light Red bg */ + --inv: #fff; + + /* ── Buttons ── */ + --btn-p-hover: #333; + --btn-p-disabled: #999; + + /* ── Status ── */ + --warn: #ff9800; + --success: #22c55e; + --info: #3b82f6; + --downloading: #f59e0b; + --error: #ef4444; + + /* ── Code blocks ── */ + --code-bg: #1e1e1e; + --code-txt: #d4d4d4; + --muted: #999; + + /* ── Overlay ── */ + --overlay: rgba(0, 0, 0, .5); + + /* ── Tag ── */ + --tag-s-bdr: rgba(255, 68, 68, .2); + --tag-shadow: rgba(0, 0, 0, .12); + + /* ── Category colors ── */ + --cat-status: #e57373; + --cat-inventory: #64b5f6; + --cat-relation: #ba68c8; + --cat-knowledge: #4db6ac; + --cat-rule: #ffd54f; + + /* ── Trend colors ── */ + --trend-broken: #444; + --trend-broken-bg: rgba(68, 68, 68, .15); + --trend-hate: #8b0000; + --trend-hate-bg: rgba(139, 0, 0, .15); + --trend-dislike: #cd5c5c; + --trend-dislike-bg: rgba(205, 92, 92, .15); + --trend-stranger: #888; + --trend-stranger-bg: rgba(136, 136, 136, .15); + --trend-click: #4a9a7e; + --trend-click-bg: rgba(102, 205, 170, .15); + --trend-close-bg: rgba(235, 106, 106, .15); + --trend-merge: #c71585; + --trend-merge-bg: rgba(199, 21, 133, .2); } -@media (prefers-color-scheme: dark) { - :root { - --bg: #111111; - --bg2: #222222; - --bg3: #333333; - --txt: #ffffff; - --txt2: #eeeeee; - --txt3: #cccccc; +:root[data-theme="dark"] { + /* ── Base ── */ + --bg: #111111; + --bg2: #222222; + --bg3: #333333; + --txt: #ffffff; + --txt2: #eeeeee; + --txt3: #cccccc; - --bdr: #ffffff; - --bdr2: #ffffff; + /* ── Neo-Brutalism Core ── */ + --bdr: #ffffff; + --bdr2: #ffffff; + --shadow: 4px 4px 0 var(--txt); + --shadow-hover: 2px 2px 0 var(--txt); + --acc: #ffffff; + --hl: #ff6b6b; + --hl2: #e07070; + --hl-soft: #442222; + --inv: #222; - --acc: #ffffff; - --hl: #ff6b6b; - --hl-soft: #442222; - } + /* ── Buttons ── */ + --btn-p-hover: #ddd; + --btn-p-disabled: #666; + + /* ── Status ── */ + --warn: #ffb74d; + --success: #4caf50; + --info: #64b5f6; + --downloading: #ffa726; + --error: #ef5350; + + /* ── Code blocks ── */ + --code-bg: #0d0d0d; + --code-txt: #d4d4d4; + --muted: #777; + + /* ── Overlay ── */ + --overlay: rgba(0, 0, 0, .7); + + /* ── Tag ── */ + --tag-s-bdr: rgba(255, 107, 107, .3); + --tag-shadow: rgba(0, 0, 0, .4); + + /* ── Category colors ── */ + --cat-status: #ef9a9a; + --cat-inventory: #90caf9; + --cat-relation: #ce93d8; + --cat-knowledge: #80cbc4; + --cat-rule: #ffe082; + + /* ── Trend colors ── */ + --trend-broken: #999; + --trend-broken-bg: rgba(153, 153, 153, .15); + --trend-hate: #ef5350; + --trend-hate-bg: rgba(239, 83, 80, .15); + --trend-dislike: #e57373; + --trend-dislike-bg: rgba(229, 115, 115, .15); + --trend-stranger: #aaa; + --trend-stranger-bg: rgba(170, 170, 170, .12); + --trend-click: #66bb6a; + --trend-click-bg: rgba(102, 187, 106, .15); + --trend-close-bg: rgba(255, 107, 107, .15); + --trend-merge: #f06292; + --trend-merge-bg: rgba(240, 98, 146, .15); } body { @@ -218,7 +314,7 @@ h1 { .stat-warning { font-size: .625rem; - color: #ff9800; + color: var(--warn); margin-top: 4px; } @@ -705,7 +801,7 @@ h1 { .prof-prog-inner { height: 100%; - background: linear-gradient(90deg, var(--hl), #d85858); + background: linear-gradient(90deg, var(--hl), var(--hl2)); border-radius: 2px; transition: width .6s; } @@ -810,38 +906,38 @@ h1 { } .trend-broken { - background: rgba(68, 68, 68, .15); - color: #444; + background: var(--trend-broken-bg); + color: var(--trend-broken); } .trend-hate { - background: rgba(139, 0, 0, .15); - color: #8b0000; + background: var(--trend-hate-bg); + color: var(--trend-hate); } .trend-dislike { - background: rgba(205, 92, 92, .15); - color: #cd5c5c; + background: var(--trend-dislike-bg); + color: var(--trend-dislike); } .trend-stranger { - background: rgba(136, 136, 136, .15); - color: #888; + background: var(--trend-stranger-bg); + color: var(--trend-stranger); } .trend-click { - background: rgba(102, 205, 170, .15); - color: #4a9a7e; + background: var(--trend-click-bg); + color: var(--trend-click); } .trend-close { - background: rgba(235, 106, 106, .15); + background: var(--trend-close-bg); color: var(--hl); } .trend-merge { - background: rgba(199, 21, 133, .2); - color: #c71585; + background: var(--trend-merge-bg); + color: var(--trend-merge); } /* ═══════════════════════════════════════════════════════════════════════════ @@ -1009,7 +1105,7 @@ h1 { } .modal-close:hover svg { - stroke: #fff; + stroke: var(--inv); } .modal-close svg { @@ -1361,24 +1457,24 @@ h1 { } .status-dot.ready { - background: #22c55e; + background: var(--success); } .status-dot.cached { - background: #3b82f6; + background: var(--info); } .status-dot.downloading { - background: #f59e0b; + background: var(--downloading); animation: pulse 1s infinite; } .status-dot.error { - background: #ef4444; + background: var(--error); } .status-dot.success { - background: #22c55e; + background: var(--success); } @keyframes pulse { @@ -1406,7 +1502,7 @@ h1 { .progress-inner { height: 100%; - background: linear-gradient(90deg, var(--hl), #d85858); + background: linear-gradient(90deg, var(--hl), var(--hl2)); border-radius: 3px; width: 0%; transition: width .3s; @@ -1445,7 +1541,7 @@ h1 { .vector-mismatch-warning { font-size: .75rem; - color: #f59e0b; + color: var(--downloading); margin-top: 6px; } @@ -1499,7 +1595,7 @@ h1 { font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; font-size: 12px; line-height: 1.6; - color: #e8e8e8; + color: var(--code-txt); white-space: pre-wrap !important; overflow-x: hidden !important; word-break: break-word; @@ -1570,7 +1666,7 @@ h1 { width: 28px; height: 28px; background: var(--acc); - color: #fff; + color: var(--inv); border-radius: 50%; display: flex; align-items: center; @@ -1660,7 +1756,7 @@ h1 { .hf-code { margin: 0; padding: 14px; - background: #1e1e1e; + background: var(--code-bg); overflow-x: auto; position: relative; } @@ -1669,7 +1765,7 @@ h1 { font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; font-size: .75rem; line-height: 1.5; - color: #d4d4d4; + color: var(--code-txt); display: block; white-space: pre; } @@ -1681,7 +1777,7 @@ h1 { padding: 4px 10px; background: rgba(255, 255, 255, .1); border: 1px solid rgba(255, 255, 255, .2); - color: #999; + color: var(--muted); font-size: .6875rem; cursor: pointer; border-radius: 4px; @@ -1690,14 +1786,14 @@ h1 { .hf-code .copy-btn:hover { background: rgba(255, 255, 255, .2); - color: #fff; + color: var(--inv); } .hf-status-badge { display: inline-block; padding: 2px 10px; background: rgba(34, 197, 94, .15); - color: #22c55e; + color: var(--success); border-radius: 10px; font-size: .75rem; } @@ -1856,23 +1952,23 @@ h1 { /* Category Icon Colors */ .world-group[data-category="status"] .world-group-title { - color: #e57373; + color: var(--cat-status); } .world-group[data-category="inventory"] .world-group-title { - color: #64b5f6; + color: var(--cat-inventory); } .world-group[data-category="relation"] .world-group-title { - color: #ba68c8; + color: var(--cat-relation); } .world-group[data-category="knowledge"] .world-group-title { - color: #4db6ac; + color: var(--cat-knowledge); } .world-group[data-category="rule"] .world-group-title { - color: #ffd54f; + color: var(--cat-rule); } /* Empty State */ @@ -1971,7 +2067,7 @@ h1 { top: 2px; width: 5px; height: 10px; - border: solid #fff; + border: solid var(--inv); border-width: 0 2px 2px 0; transform: rotate(45deg); } @@ -2205,8 +2301,8 @@ h1 { ═══════════════════════════════════════════════════════════════════════════ */ .debug-log-viewer { - background: #1a1a1a; - color: #e0e0e0; + background: var(--code-bg); + color: var(--code-txt); padding: 16px; border-radius: 8px; font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; @@ -2221,7 +2317,7 @@ h1 { } .recall-empty { - color: #999; + color: var(--muted); text-align: center; padding: 40px; font-style: italic; @@ -2234,15 +2330,15 @@ h1 { ═══════════════════════════════════════════════════════════════════════════ */ #recall-log-content .metric-warn { - color: #f59e0b; + color: var(--downloading); } #recall-log-content .metric-error { - color: #ef4444; + color: var(--error); } #recall-log-content .metric-good { - color: #22c55e; + color: var(--success); } /* ═══════════════════════════════════════════════════════════════════════════ @@ -2485,8 +2581,8 @@ h1 { .neo-badge { /* Explicitly requested Black Background & White Text */ - background: #000; - color: #fff; + background: var(--acc); + color: var(--inv); padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index 5dca268..6a4d5cb 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -358,8 +358,8 @@ postMsg('ANCHOR_GENERATE'); }; - $('btn-anchor-clear').onclick = () => { - if (confirm('清空所有记忆锚点?(L0 向量也会一并清除)')) { + $('btn-anchor-clear').onclick = async () => { + if (await showConfirm('清空锚点', '清空所有记忆锚点?(L0 向量也会一并清除)')) { postMsg('ANCHOR_CLEAR'); } }; @@ -375,6 +375,7 @@ }; $('btn-test-vector-api').onclick = () => { + saveConfig(); // 先保存新 Key 到 localStorage postMsg('VECTOR_TEST_ONLINE', { provider: 'siliconflow', config: { @@ -391,8 +392,10 @@ postMsg('VECTOR_GENERATE', { config: getVectorConfig() }); }; - $('btn-clear-vectors').onclick = () => { - if (confirm('确定清空所有向量数据?')) postMsg('VECTOR_CLEAR'); + $('btn-clear-vectors').onclick = async () => { + if (await showConfirm('清空向量', '确定清空所有向量数据?')) { + postMsg('VECTOR_CLEAR'); + } }; $('btn-cancel-vectors').onclick = () => postMsg('VECTOR_CANCEL_GENERATE'); @@ -955,6 +958,43 @@ postMsg('FULLSCREEN_CLOSED'); } + /** + * 显示通用确认弹窗 + * @returns {Promise} + */ + function showConfirm(title, message, okText = '执行', cancelText = '取消') { + return new Promise(resolve => { + const modal = $('confirm-modal'); + const titleEl = $('confirm-title'); + const msgEl = $('confirm-message'); + const okBtn = $('confirm-ok'); + const cancelBtn = $('confirm-cancel'); + const closeBtn = $('confirm-close'); + const backdrop = $('confirm-backdrop'); + + titleEl.textContent = title; + msgEl.textContent = message; + okBtn.textContent = okText; + cancelBtn.textContent = cancelText; + + const close = (result) => { + modal.classList.remove('active'); + okBtn.onclick = null; + cancelBtn.onclick = null; + closeBtn.onclick = null; + backdrop.onclick = null; + resolve(result); + }; + + okBtn.onclick = () => close(true); + cancelBtn.onclick = () => close(false); + closeBtn.onclick = () => close(false); + backdrop.onclick = () => close(false); + + modal.classList.add('active'); + }); + } + function renderArcsEditor(arcs) { const list = arcs?.length ? arcs : [{ name: '', trajectory: '', progress: 0, moments: [] }]; const es = $('editor-struct'); @@ -1526,7 +1566,11 @@ }; // Main actions - $('btn-clear').onclick = () => postMsg('REQUEST_CLEAR'); + $('btn-clear').onclick = async () => { + if (await showConfirm('清空数据', '确定要清空本聊天的所有总结、关键词及人物关系数据吗?此操作不可撤销。')) { + postMsg('REQUEST_CLEAR'); + } + }; $('btn-generate').onclick = () => { const btn = $('btn-generate'); if (!localGenerating) { @@ -1640,42 +1684,34 @@ bindEvents(); - // === EASTER EGG: 连续点击标题「总结」5 次切换新野兽派主题(localStorage 持久化)=== + // === THEME SWITCHER === (function () { const STORAGE_KEY = 'xb-theme-alt'; - const CSS_A = 'story-summary.css'; - const CSS_B = 'story-summary-a.css'; + const CSS_MAP = { default: 'story-summary.css', dark: 'story-summary.css', neo: 'story-summary-a.css', 'neo-dark': 'story-summary-a.css' }; const link = document.querySelector('link[rel="stylesheet"]'); - if (!link) return; + const sel = document.getElementById('theme-select'); + if (!link || !sel) return; - // 启动时:根据持久化状态设置 CSS - if (localStorage.getItem(STORAGE_KEY) === '1') { - link.setAttribute('href', CSS_B); + function applyTheme(theme) { + if (!CSS_MAP[theme]) return; + link.setAttribute('href', CSS_MAP[theme]); + document.documentElement.setAttribute('data-theme', (theme === 'dark' || theme === 'neo-dark') ? 'dark' : ''); } - // 点击计数器 - let clickCount = 0, clickTimer = null; - const trigger = document.querySelector('h1 span'); - if (!trigger) return; + // 启动时恢复主题 + const saved = localStorage.getItem(STORAGE_KEY) || 'default'; + applyTheme(saved); + sel.value = saved; - trigger.style.cursor = 'default'; - trigger.addEventListener('click', function () { - clickCount++; - clearTimeout(clickTimer); - clickTimer = setTimeout(() => { clickCount = 0; }, 2000); - - if (clickCount >= 5) { - clickCount = 0; - clearTimeout(clickTimer); - const isAlt = localStorage.getItem(STORAGE_KEY) === '1'; - const next = isAlt ? CSS_A : CSS_B; - localStorage.setItem(STORAGE_KEY, isAlt ? '0' : '1'); - link.setAttribute('href', next); - console.log(`[Easter Egg] Theme toggled → ${next}`); - } + // 下拉框切换 + sel.addEventListener('change', function () { + const theme = sel.value; + applyTheme(theme); + localStorage.setItem(STORAGE_KEY, theme); + console.log(`[Theme] Switched → ${theme} (${CSS_MAP[theme]})`); }); })(); - // === END EASTER EGG === + // === END THEME SWITCHER === // Notify parent postMsg('FRAME_READY'); diff --git a/modules/story-summary/story-summary.css b/modules/story-summary/story-summary.css index 403bc1d..a7d8b2b 100644 --- a/modules/story-summary/story-summary.css +++ b/modules/story-summary/story-summary.css @@ -20,6 +20,10 @@ padding-right: 4px; } +.confirm-modal-box { + max-width: 440px; +} + .fact-group { margin-bottom: 12px; } @@ -80,6 +84,7 @@ } :root { + /* ── Base ── */ --bg: #fafafa; --bg2: #fff; --bg3: #f5f5f5; @@ -90,7 +95,117 @@ --bdr2: #e8e8e8; --acc: #1a1a1a; --hl: #d87a7a; + --hl2: #d85858; --hl-soft: rgba(184, 90, 90, .1); + --inv: #fff; + /* text on accent/primary bg */ + + /* ── Buttons ── */ + --btn-p-hover: #555; + --btn-p-disabled: #999; + + /* ── Status ── */ + --warn: #ff9800; + --success: #22c55e; + --info: #3b82f6; + --downloading: #f59e0b; + --error: #ef4444; + + /* ── Code blocks ── */ + --code-bg: #1e1e1e; + --code-txt: #d4d4d4; + --muted: #999; + + /* ── Overlay ── */ + --overlay: rgba(0, 0, 0, .5); + + /* ── Tag highlight border ── */ + --tag-s-bdr: rgba(255, 68, 68, .2); + --tag-shadow: rgba(0, 0, 0, .08); + + /* ── Category colors ── */ + --cat-status: #e57373; + --cat-inventory: #64b5f6; + --cat-relation: #ba68c8; + --cat-knowledge: #4db6ac; + --cat-rule: #ffd54f; + + /* ── Trend colors ── */ + --trend-broken: #444; + --trend-broken-bg: rgba(68, 68, 68, .15); + --trend-hate: #8b0000; + --trend-hate-bg: rgba(139, 0, 0, .15); + --trend-dislike: #cd5c5c; + --trend-dislike-bg: rgba(205, 92, 92, .15); + --trend-stranger: #888; + --trend-stranger-bg: rgba(136, 136, 136, .15); + --trend-click: #4a9a7e; + --trend-click-bg: rgba(102, 205, 170, .15); + --trend-close-bg: rgba(235, 106, 106, .15); + --trend-merge: #c71585; + --trend-merge-bg: rgba(199, 21, 133, .2); +} + +:root[data-theme="dark"] { + /* ── Base ── */ + --bg: #121212; + --bg2: #1e1e1e; + --bg3: #2a2a2a; + --txt: #e0e0e0; + --txt2: #b0b0b0; + --txt3: #808080; + --bdr: #3a3a3a; + --bdr2: #333; + --acc: #e0e0e0; + --hl: #e8928a; + --hl2: #e07070; + --hl-soft: rgba(232, 146, 138, .12); + --inv: #1e1e1e; + + /* ── Buttons ── */ + --btn-p-hover: #ccc; + --btn-p-disabled: #666; + + /* ── Status ── */ + --warn: #ffb74d; + --success: #4caf50; + --info: #64b5f6; + --downloading: #ffa726; + --error: #ef5350; + + /* ── Code blocks ── */ + --code-bg: #0d0d0d; + --code-txt: #d4d4d4; + --muted: #777; + + /* ── Overlay ── */ + --overlay: rgba(0, 0, 0, .7); + + /* ── Tag ── */ + --tag-s-bdr: rgba(232, 146, 138, .3); + --tag-shadow: rgba(0, 0, 0, .3); + + /* ── Category colors (softer for dark) ── */ + --cat-status: #ef9a9a; + --cat-inventory: #90caf9; + --cat-relation: #ce93d8; + --cat-knowledge: #80cbc4; + --cat-rule: #ffe082; + + /* ── Trend colors ── */ + --trend-broken: #999; + --trend-broken-bg: rgba(153, 153, 153, .15); + --trend-hate: #ef5350; + --trend-hate-bg: rgba(239, 83, 80, .15); + --trend-dislike: #e57373; + --trend-dislike-bg: rgba(229, 115, 115, .15); + --trend-stranger: #aaa; + --trend-stranger-bg: rgba(170, 170, 170, .12); + --trend-click: #66bb6a; + --trend-click-bg: rgba(102, 187, 106, .15); + --trend-close-bg: rgba(232, 146, 138, .15); + --trend-merge: #f06292; + --trend-merge-bg: rgba(240, 98, 146, .15); } body { @@ -204,7 +319,7 @@ h1 span { .stat-warning { font-size: .625rem; - color: #ff9800; + color: var(--warn); margin-top: 4px; } @@ -306,17 +421,17 @@ h1 span { .btn-p { background: var(--acc); - color: #fff; + color: var(--inv); border-color: var(--acc); } .btn-p:hover { - background: #555; + background: var(--btn-p-hover); } .btn-p:disabled { - background: #999; - border-color: #999; + background: var(--btn-p-disabled); + border-color: var(--btn-p-disabled); cursor: not-allowed; opacity: .7; } @@ -466,20 +581,20 @@ h1 span { .tag.p { background: var(--acc); - color: #fff; + color: var(--inv); border-color: var(--acc); font-weight: 500; } .tag.s { background: var(--hl-soft); - border-color: rgba(255, 68, 68, .2); + border-color: var(--tag-s-bdr); color: var(--hl); } .tag:hover { transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, .08); + box-shadow: 0 4px 12px var(--tag-shadow); } /* ═══════════════════════════════════════════════════════════════════════════ @@ -662,7 +777,7 @@ h1 span { .prof-prog-inner { height: 100%; - background: linear-gradient(90deg, var(--hl), #d85858); + background: linear-gradient(90deg, var(--hl), var(--hl2)); border-radius: 2px; transition: width .6s; } @@ -769,38 +884,38 @@ h1 span { } .trend-broken { - background: rgba(68, 68, 68, .15); - color: #444; + background: var(--trend-broken-bg); + color: var(--trend-broken); } .trend-hate { - background: rgba(139, 0, 0, .15); - color: #8b0000; + background: var(--trend-hate-bg); + color: var(--trend-hate); } .trend-dislike { - background: rgba(205, 92, 92, .15); - color: #cd5c5c; + background: var(--trend-dislike-bg); + color: var(--trend-dislike); } .trend-stranger { - background: rgba(136, 136, 136, .15); - color: #888; + background: var(--trend-stranger-bg); + color: var(--trend-stranger); } .trend-click { - background: rgba(102, 205, 170, .15); - color: #4a9a7e; + background: var(--trend-click-bg); + color: var(--trend-click); } .trend-close { - background: rgba(235, 106, 106, .15); + background: var(--trend-close-bg); color: var(--hl); } .trend-merge { - background: rgba(199, 21, 133, .2); - color: #c71585; + background: var(--trend-merge-bg); + color: var(--trend-merge); } /* ═══════════════════════════════════════════════════════════════════════════ @@ -913,7 +1028,7 @@ h1 span { .modal-bg { position: absolute; inset: 0; - background: rgba(0, 0, 0, .5); + background: var(--overlay); backdrop-filter: blur(4px); } @@ -964,6 +1079,7 @@ h1 span { .modal-close svg { width: 14px; height: 14px; + color: var(--txt); } .modal-body { @@ -1031,7 +1147,7 @@ h1 span { .editor-err { padding: 12px; background: var(--hl-soft); - border: 1px solid rgba(255, 68, 68, .3); + border: 1px solid var(--tag-s-bdr); color: var(--hl); font-size: .8125rem; margin-top: 12px; @@ -1301,24 +1417,24 @@ h1 span { } .status-dot.ready { - background: #22c55e; + background: var(--success); } .status-dot.cached { - background: #3b82f6; + background: var(--info); } .status-dot.downloading { - background: #f59e0b; + background: var(--downloading); animation: pulse 1s infinite; } .status-dot.error { - background: #ef4444; + background: var(--error); } .status-dot.success { - background: #22c55e; + background: var(--success); } @keyframes pulse { @@ -1346,7 +1462,7 @@ h1 span { .progress-inner { height: 100%; - background: linear-gradient(90deg, var(--hl), #d85858); + background: linear-gradient(90deg, var(--hl), var(--hl2)); border-radius: 3px; width: 0%; transition: width .3s; @@ -1404,7 +1520,7 @@ h1 span { .vector-mismatch-warning { font-size: .75rem; - color: #f59e0b; + color: var(--downloading); margin-top: 6px; } @@ -1458,7 +1574,7 @@ h1 span { font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; font-size: 12px; line-height: 1.6; - color: #e8e8e8; + color: var(--code-txt); white-space: pre-wrap !important; overflow-x: hidden !important; word-break: break-word; @@ -1468,7 +1584,7 @@ h1 span { } .recall-empty { - color: #999; + color: var(--muted); text-align: center; padding: 40px; font-style: italic; @@ -1555,7 +1671,7 @@ h1 span { width: 28px; height: 28px; background: var(--acc); - color: #fff; + color: var(--inv); border-radius: 50%; display: flex; align-items: center; @@ -1648,7 +1764,7 @@ h1 span { .hf-code { margin: 0; padding: 14px; - background: #1e1e1e; + background: var(--code-bg); overflow-x: auto; position: relative; } @@ -1657,7 +1773,7 @@ h1 span { font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; font-size: .75rem; line-height: 1.5; - color: #d4d4d4; + color: var(--code-txt); display: block; white-space: pre; } @@ -1669,7 +1785,7 @@ h1 span { padding: 4px 10px; background: rgba(255, 255, 255, .1); border: 1px solid rgba(255, 255, 255, .2); - color: #999; + color: var(--muted); font-size: .6875rem; cursor: pointer; border-radius: 4px; @@ -1678,14 +1794,14 @@ h1 span { .hf-code .copy-btn:hover { background: rgba(255, 255, 255, .2); - color: #fff; + color: var(--inv); } .hf-status-badge { display: inline-block; padding: 2px 10px; background: rgba(34, 197, 94, .15); - color: #22c55e; + color: var(--success); border-radius: 10px; font-size: .75rem; font-weight: 500; @@ -2291,23 +2407,23 @@ h1 span { /* 分类图标颜色 */ .world-group[data-category="status"] .world-group-title { - color: #e57373; + color: var(--cat-status); } .world-group[data-category="inventory"] .world-group-title { - color: #64b5f6; + color: var(--cat-inventory); } .world-group[data-category="relation"] .world-group-title { - color: #ba68c8; + color: var(--cat-relation); } .world-group[data-category="knowledge"] .world-group-title { - color: #4db6ac; + color: var(--cat-knowledge); } .world-group[data-category="rule"] .world-group-title { - color: #ffd54f; + color: var(--cat-rule); } /* 空状态 */ @@ -2444,7 +2560,7 @@ h1 span { top: 2px; width: 5px; height: 10px; - border: solid #fff; + border: solid var(--inv); border-width: 0 2px 2px 0; transform: rotate(45deg); } @@ -2740,8 +2856,8 @@ h1 span { ═══════════════════════════════════════════════════════════════════════════ */ .debug-log-viewer { - background: #1a1a1a; - color: #e0e0e0; + background: var(--code-bg); + color: var(--code-txt); padding: 16px; border-radius: 8px; font-family: 'Consolas', 'Monaco', 'SF Mono', monospace; @@ -2756,7 +2872,7 @@ h1 span { } .recall-empty { - color: #999; + color: var(--muted); text-align: center; padding: 40px; font-style: italic; @@ -2775,15 +2891,15 @@ h1 span { ═══════════════════════════════════════════════════════════════════════════ */ #recall-log-content .metric-warn { - color: #f59e0b; + color: var(--downloading); } #recall-log-content .metric-error { - color: #ef4444; + color: var(--error); } #recall-log-content .metric-good { - color: #22c55e; + color: var(--success); } /* ═══════════════════════════════════════════════════════════════════════════ @@ -2825,7 +2941,7 @@ h1 span { width: 26px; height: 26px; background: var(--acc); - color: #fff; + color: var(--inv); border-radius: 50%; display: flex; align-items: center; @@ -2872,7 +2988,7 @@ h1 span { width: 22px; height: 22px; background: var(--hl); - color: #fff; + color: var(--inv); border-radius: 50%; display: flex; align-items: center; @@ -3103,7 +3219,7 @@ h1 span { width: 18px; height: 18px; background: var(--acc); - color: #fff; + color: var(--inv); border-radius: 3px; font-size: .625rem; font-weight: 700; @@ -3304,8 +3420,8 @@ h1 span { .neo-badge { /* Explicitly requested Black Background & White Text */ - background: #000; - color: #fff; + background: var(--acc); + color: var(--inv); padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index 2a0f148..4e92395 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -176,6 +176,22 @@