story-summary: facts UI split + relationships from facts

This commit is contained in:
2026-02-02 22:48:07 +08:00
parent fb8ed8037c
commit 1128d1494e
5 changed files with 67 additions and 75 deletions

View File

@@ -76,6 +76,27 @@ export function isRelationFact(f) {
return /^对.+的/.test(f.p); 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 * 生成 fact 的唯一键s + p
*/ */

View File

@@ -1867,41 +1867,28 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
const container = $('facts-list'); const container = $('facts-list');
if (!container) return; 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) { if (!stateFacts.length) {
setHtml(container, '<div class="empty">暂无事实记录</div>'); setHtml(container, '<div class="empty">暂无状态记录</div>');
return; return;
} }
const relations = activeFacts.filter(f => /^.+/.test(f.p)); const grouped = new Map();
const states = activeFacts.filter(f => !/^对.+的/.test(f.p)); for (const f of stateFacts) {
if (!grouped.has(f.s)) grouped.set(f.s, []);
let html = ''; grouped.get(f.s).push(f);
if (states.length) {
html += `<div class="fact-group">
<div class="fact-group-title">状态/属性</div>
${states.map(f => `
<div class="fact-item">
<span class="fact-subject">${h(f.s)}</span>
<span class="fact-predicate">${h(f.p)}</span>
<span class="fact-object">${h(f.o)}</span>
<span class="fact-since">#${(f.since || 0) + 1}</span>
</div>
`).join('')}
</div>`;
} }
if (relations.length) { let html = '';
for (const [subject, items] of grouped) {
html += `<div class="fact-group"> html += `<div class="fact-group">
<div class="fact-group-title">人物关系</div> <div class="fact-group-title">${h(subject)}</div>
${relations.map(f => ` ${items.map(f => `
<div class="fact-item"> <div class="fact-item">
<span class="fact-subject">${h(f.s)}</span>
<span class="fact-predicate">${h(f.p)}</span> <span class="fact-predicate">${h(f.p)}</span>
<span class="fact-object">${h(f.o)}</span> <span class="fact-object">${h(f.o)}</span>
${f.trend ? `<span class="fact-trend ${TREND_CLASS[f.trend] || ''}">${h(f.trend)}</span>` : ''}
<span class="fact-since">#${(f.since || 0) + 1}</span> <span class="fact-since">#${(f.since || 0) + 1}</span>
</div> </div>
`).join('')} `).join('')}
@@ -1909,5 +1896,5 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
} }
setHtml(container, html); setHtml(container, html);
} }
})(); })();

View File

@@ -21,7 +21,7 @@
} }
.fact-group { .fact-group {
margin-bottom: 16px; margin-bottom: 12px;
} }
.fact-group:last-child { .fact-group:last-child {
@@ -29,65 +29,43 @@
} }
.fact-group-title { .fact-group-title {
font-size: 0.6875rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
text-transform: uppercase; color: var(--hl);
letter-spacing: 0.08em; margin-bottom: 6px;
color: var(--txt3); padding-bottom: 4px;
margin-bottom: 8px; border-bottom: 1px dashed var(--bdr2);
padding-bottom: 6px;
border-bottom: 1px solid var(--bdr2);
} }
.fact-item { .fact-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
padding: 8px 10px; padding: 6px 10px;
margin-bottom: 6px; margin-bottom: 4px;
background: var(--bg3); background: var(--bg3);
border: 1px solid var(--bdr2); border: 1px solid var(--bdr2);
border-radius: 6px; border-radius: 4px;
font-size: 0.8125rem; 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 { .fact-predicate {
color: var(--txt3); color: var(--txt2);
font-size: 0.75rem; min-width: 60px;
} }
.fact-predicate::before { .fact-predicate::after {
content: ''; content: '';
margin-right: 4px;
} }
.fact-object { .fact-object {
color: var(--hl); color: var(--txt);
font-weight: 500; flex: 1;
}
.fact-trend {
font-size: 0.6875rem;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
} }
.fact-since { .fact-since {
font-size: 0.625rem; font-size: 0.625rem;
color: var(--txt3); color: var(--txt3);
margin-left: auto;
} }
@media (max-width: 768px) { @media (max-width: 768px) {

View File

@@ -83,7 +83,7 @@
<!-- Facts --> <!-- Facts -->
<section class="card facts"> <section class="card facts">
<div class="sec-head"> <div class="sec-head">
<div class="sec-title">事实图谱</div> <div class="sec-title">世界状态</div>
<button class="sec-btn" data-section="facts">编辑</button> <button class="sec-btn" data-section="facts">编辑</button>
</div> </div>
<div class="facts-list scroll" id="facts-list"></div> <div class="facts-list scroll" id="facts-list"></div>

View File

@@ -30,6 +30,7 @@ import {
calcHideRange, calcHideRange,
rollbackSummaryIfNeeded, rollbackSummaryIfNeeded,
clearSummaryData, clearSummaryData,
extractRelationshipsFromFacts,
} from "./data/store.js"; } from "./data/store.js";
// prompt text builder // prompt text builder
@@ -848,14 +849,19 @@ async function sendFrameBaseData(store, totalFloors) {
function sendFrameFullData(store, totalFloors) { function sendFrameFullData(store, totalFloors) {
const lastSummarized = store?.lastSummarizedMesId ?? -1; const lastSummarized = store?.lastSummarizedMesId ?? -1;
if (store?.json) { if (store?.json) {
const facts = store.json.facts || [];
const relationships = extractRelationshipsFromFacts(facts);
postToFrame({ postToFrame({
type: "SUMMARY_FULL_DATA", type: "SUMMARY_FULL_DATA",
payload: { payload: {
keywords: store.json.keywords || [], keywords: store.json.keywords || [],
events: store.json.events || [], events: store.json.events || [],
characters: store.json.characters || { main: [], relationships: [] }, characters: {
main: store.json.characters?.main || [],
relationships,
},
arcs: store.json.arcs || [], arcs: store.json.arcs || [],
world: store.json.world || [], facts,
lastSummarizedMesId: lastSummarized, lastSummarizedMesId: lastSummarized,
}, },
}); });