From 1128d1494e13b189aeb3553e743d963533be9deb Mon Sep 17 00:00:00 2001 From: bielie Date: Mon, 2 Feb 2026 22:48:07 +0800 Subject: [PATCH] story-summary: facts UI split + relationships from facts --- modules/story-summary/data/store.js | 21 +++++++++ modules/story-summary/story-summary-ui.js | 55 +++++++++-------------- modules/story-summary/story-summary.css | 54 +++++++--------------- modules/story-summary/story-summary.html | 2 +- modules/story-summary/story-summary.js | 10 ++++- 5 files changed, 67 insertions(+), 75 deletions(-) diff --git a/modules/story-summary/data/store.js b/modules/story-summary/data/store.js index 366ea97..0f96255 100644 --- a/modules/story-summary/data/store.js +++ b/modules/story-summary/data/store.js @@ -76,6 +76,27 @@ export function isRelationFact(f) { return /^对.+的/.test(f.p); } +// ═══════════════════════════════════════════════════════════════════════════ +// 从 facts 提取关系(供关系图 UI 使用) +// ═══════════════════════════════════════════════════════════════════════════ + +export function extractRelationshipsFromFacts(facts) { + return (facts || []) + .filter(f => !f.retracted && isRelationFact(f)) + .map(f => { + const match = f.p.match(/^对(.+)的/); + const to = match ? match[1] : ''; + if (!to) return null; + return { + from: f.s, + to, + label: f.o, + trend: f.trend || '陌生', + }; + }) + .filter(Boolean); +} + /** * 生成 fact 的唯一键(s + p) */ diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index c4f25b1..14c898e 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -1862,52 +1862,39 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", " function renderFacts(facts) { - summaryData.facts = facts || []; + summaryData.facts = facts || []; - const container = $('facts-list'); - if (!container) return; + const container = $('facts-list'); + if (!container) return; - const activeFacts = (facts || []).filter(f => !f.retracted); + const isRelation = f => /^对.+的/.test(f.p); + const stateFacts = (facts || []).filter(f => !f.retracted && !isRelation(f)); - if (!activeFacts.length) { - setHtml(container, '
暂无事实记录
'); - return; - } + if (!stateFacts.length) { + setHtml(container, '
暂无状态记录
'); + return; + } - const relations = activeFacts.filter(f => /^对.+的/.test(f.p)); - const states = activeFacts.filter(f => !/^对.+的/.test(f.p)); + const grouped = new Map(); + for (const f of stateFacts) { + if (!grouped.has(f.s)) grouped.set(f.s, []); + grouped.get(f.s).push(f); + } - let html = ''; - - if (states.length) { - html += `
-
状态/属性
- ${states.map(f => ` + let html = ''; + for (const [subject, items] of grouped) { + html += `
+
${h(subject)}
+ ${items.map(f => `
- ${h(f.s)} ${h(f.p)} ${h(f.o)} #${(f.since || 0) + 1}
`).join('')}
`; - } + } - if (relations.length) { - html += `
-
人物关系
- ${relations.map(f => ` -
- ${h(f.s)} - ${h(f.p)} - ${h(f.o)} - ${f.trend ? `${h(f.trend)}` : ''} - #${(f.since || 0) + 1} -
- `).join('')} -
`; + setHtml(container, html); } - - setHtml(container, html); -} })(); diff --git a/modules/story-summary/story-summary.css b/modules/story-summary/story-summary.css index cb70f02..6fe71c0 100644 --- a/modules/story-summary/story-summary.css +++ b/modules/story-summary/story-summary.css @@ -21,7 +21,7 @@ } .fact-group { - margin-bottom: 16px; + margin-bottom: 12px; } .fact-group:last-child { @@ -29,65 +29,43 @@ } .fact-group-title { - font-size: 0.6875rem; + font-size: 0.75rem; font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--txt3); - margin-bottom: 8px; - padding-bottom: 6px; - border-bottom: 1px solid var(--bdr2); + color: var(--hl); + margin-bottom: 6px; + padding-bottom: 4px; + border-bottom: 1px dashed var(--bdr2); } .fact-item { display: flex; align-items: center; - gap: 6px; - padding: 8px 10px; - margin-bottom: 6px; + gap: 8px; + padding: 6px 10px; + margin-bottom: 4px; background: var(--bg3); border: 1px solid var(--bdr2); - border-radius: 6px; + border-radius: 4px; font-size: 0.8125rem; - flex-wrap: wrap; -} - -.fact-item:hover { - border-color: var(--bdr); - background: var(--bg2); -} - -.fact-subject { - font-weight: 600; - color: var(--txt); } .fact-predicate { - color: var(--txt3); - font-size: 0.75rem; + color: var(--txt2); + min-width: 60px; } -.fact-predicate::before { - content: '→'; - margin-right: 4px; +.fact-predicate::after { + content: ':'; } .fact-object { - color: var(--hl); - font-weight: 500; -} - -.fact-trend { - font-size: 0.6875rem; - padding: 2px 8px; - border-radius: 10px; - white-space: nowrap; + color: var(--txt); + flex: 1; } .fact-since { font-size: 0.625rem; color: var(--txt3); - margin-left: auto; } @media (max-width: 768px) { diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index 405e206..e0f7e2b 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -83,7 +83,7 @@
-
事实图谱
+
世界状态
diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 09120e2..cfb95a5 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -30,6 +30,7 @@ import { calcHideRange, rollbackSummaryIfNeeded, clearSummaryData, + extractRelationshipsFromFacts, } from "./data/store.js"; // prompt text builder @@ -848,14 +849,19 @@ async function sendFrameBaseData(store, totalFloors) { function sendFrameFullData(store, totalFloors) { const lastSummarized = store?.lastSummarizedMesId ?? -1; if (store?.json) { + const facts = store.json.facts || []; + const relationships = extractRelationshipsFromFacts(facts); postToFrame({ type: "SUMMARY_FULL_DATA", payload: { keywords: store.json.keywords || [], events: store.json.events || [], - characters: store.json.characters || { main: [], relationships: [] }, + characters: { + main: store.json.characters?.main || [], + relationships, + }, arcs: store.json.arcs || [], - world: store.json.world || [], + facts, lastSummarizedMesId: lastSummarized, }, });