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);
}
// ═══════════════════════════════════════════════════════════════════════════
// 从 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
*/

View File

@@ -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, '<div class="empty">暂无事实记录</div>');
return;
}
if (!stateFacts.length) {
setHtml(container, '<div class="empty">暂无状态记录</div>');
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 += `<div class="fact-group">
<div class="fact-group-title">状态/属性</div>
${states.map(f => `
let html = '';
for (const [subject, items] of grouped) {
html += `<div class="fact-group">
<div class="fact-group-title">${h(subject)}</div>
${items.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) {
html += `<div class="fact-group">
<div class="fact-group-title">人物关系</div>
${relations.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>
${f.trend ? `<span class="fact-trend ${TREND_CLASS[f.trend] || ''}">${h(f.trend)}</span>` : ''}
<span class="fact-since">#${(f.since || 0) + 1}</span>
</div>
`).join('')}
</div>`;
setHtml(container, html);
}
setHtml(container, html);
}
})();

View File

@@ -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) {

View File

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

View File

@@ -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,
},
});