feat: add app management, local inference controls, and task statistics
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# 故事线图谱(Story Graph)
|
||||
|
||||
这是[监督者应用](../conversation-supervision/supervisor-prd.md)的核心视图。目标对象与交互由[产品设计](./prd.md)定义,用户验收见[User Stories](../conversation-supervision/user-stories.md)。时间事件、知识实体与阶段总结在目标设计中分别建模;下述演示仍使用阶段节点。
|
||||
|
||||
[打开交互演示](./demo.html)。这是监督者故事图谱的独立设计原型,使用模拟数据,尚未接入产品或笔记存储。
|
||||
|
||||
`demo.html`、`demo.css`、`demo.js` 共同负责可交互的界面草案:时间线选择、阶段回放、跨时间线关联、节点总结和来源展开。直接在浏览器中打开 HTML 即可使用,无需安装依赖。
|
||||
|
||||
当前设计假设:每个圆代表一条时间线,实心节点代表阶段总结,实线表示同一时间线的先后关系,虚线表示不同时间线间的知识关联。节点位置为示意布局,尚未定义集中度算法,不代表效率或产出。聚合知识以预置示例演示,不调用模型。
|
||||
|
||||
新增平铺视图(默认):时间轴沿画布外围逆时针环绕,从顶部 09.01 经左侧 09.07、底部到右侧 09.14,以箭头表示继续推进。知识节点平铺在环内,外圈时间点与对应阶段节点直接连接,选择节点时高亮其时间连线。工具栏可切回圆形视图,保留所选阶段、时间线与节点。当前仍使用阶段节点示例,同一知识实体在多个时间点发生修订的合并展示尚未实现。
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Story Graph · GoodBuddy 监督者</title>
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="appbar"><div class="brand"><span class="brandmark">G</span> GoodBuddy <span class="separator">/</span> 监督者</div><div class="top-actions"><span class="badge">交互原型 · 模拟数据</span><button id="theme" aria-label="切换深浅主题">切换主题</button></div></header>
|
||||
<main>
|
||||
<header class="page-heading"><div><div class="eyebrow">STORY GRAPH</div><h1>故事图谱</h1><p>沿着工作的来路,看见知识之间的联系。</p></div><div class="scope">范围:GoodBuddy 项目<br><span>2026.09.01 — 09.14</span></div></header>
|
||||
<div class="workspace">
|
||||
<aside class="timelines"><div class="section-heading"><h2>时间线</h2><span>03</span></div><p class="muted">选择一条故事,查看它的演变。</p><div id="timeline-list"></div><div class="sidebar-note"><span class="eyebrow">如何读图</span><p>一个圆,一条时间线。<br>圆内节点是阶段总结,<br>圆间虚线是知识关联。</p><p class="muted">分布仅作示意。集中度不等于效率或产出。</p></div></aside>
|
||||
<section class="graph-section" aria-label="故事图谱画布"><div class="graph-toolbar"><div><strong id="graph-title">全部故事</strong><span id="node-count" class="muted"></span></div><label class="view-picker">视图 <select id="view" aria-label="图谱视图"><option value="flat">平铺 · 环绕时间轴</option><option value="circles">圆形 · 独立故事</option></select></label><label class="check"><input id="relations" type="checkbox" checked> 跨线关联</label></div>
|
||||
<div class="canvas"><svg id="graph" viewBox="0 0 860 620" role="group" aria-label="可交互故事图谱,使用 Tab 选择阶段节点"></svg><div class="canvas-caption"><span class="live-dot"></span> 阶段总结构成故事,关联让知识相遇</div></div>
|
||||
<div class="graph-legend"><span><i class="dot"></i>阶段总结</span><span><i class="line"></i>时间推进</span><span><i class="line dashed"></i>知识关联</span><button id="reset">显示全部时间线</button></div>
|
||||
<div class="playback"><div class="playback-heading"><strong>故事如何长出来</strong><output id="date-label" for="stage">09.14 · 知识汇聚</output></div><input id="stage" type="range" min="0" max="2" value="2" step="1" aria-label="回放阶段"><div class="range-labels"><span>09.01 · 起点</span><span>09.07 · 探索</span><span>09.14 · 汇聚</span></div></div>
|
||||
</section>
|
||||
<aside class="inspector" aria-label="阶段详情"><div class="section-heading"><h2>故事详情</h2><span class="badge">可追溯</span></div><div id="detail" aria-live="polite"></div></aside>
|
||||
</div>
|
||||
<section class="insights"><div><div class="eyebrow">ACROSS STORIES</div><h2>从不同故事中,找到同一个答案</h2><p>3 条时间线中,有一条共同的知识线索。</p></div><button id="insight" class="insight-card"><span class="insight-icon">↗</span><span><strong>知识需要保留来路</strong><small>故事图谱 × 个人知识库 × 记忆架构</small></span><span>查看聚合 →</span></button></section>
|
||||
<footer>设计讨论稿 · 所有总结、关联和来源均为模拟示例;未读取或修改真实会话。</footer>
|
||||
</main>
|
||||
<script src="demo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,119 @@
|
||||
/* global document */
|
||||
const stories = [
|
||||
{ id: 'graph', title: '监督者 · 故事图谱', color: 'var(--accent)', cx: 270, cy: 230, r: 172, description: '从工作过程到可追溯的故事' },
|
||||
{ id: 'knowledge', title: '个人知识库', color: 'var(--success)', cx: 625, cy: 260, r: 146, description: '从学习记录到可复用的知识' },
|
||||
{ id: 'memory', title: '记忆架构', color: 'var(--warning)', cx: 451, cy: 463, r: 118, description: '从原始数据到分层理解' }
|
||||
];
|
||||
const nodes = [
|
||||
{ id: 'g0', story: 'graph', stage: 0, x: 160, y: 205, label: '观察工作过程', title: '让工作过程可以回看', copy: '监督者按时间记录工作的推进。我们希望在记录之上保留阶段性的理解,帮助用户重新进入一段未完成的工作。', source: '监督者功能讨论', quote: '过一周再回来,我需要知道之前为什么这样做,而不只是看到最后的结果。' },
|
||||
{ id: 'g1', story: 'graph', stage: 1, x: 261, y: 260, label: '阶段总结', title: '把连续记录整理成阶段', copy: '每个阶段保留讨论的问题、形成的判断和仍未解决的事项。阶段节点沿时间连接,组成一条能够继续展开的故事。', source: '阶段总结交互草案', quote: '以一个阶段为单位总结。保留关键决定,也保留没有走通的尝试和原始记录。' },
|
||||
{ id: 'g2', story: 'graph', stage: 2, x: 326, y: 215, label: '保留知识来路', title: '总结之后,仍能找到来路', copy: '故事图谱把阶段总结和原始记录连接起来。跨时间线聚合出的知识,也应保留每条贡献来源,方便核对结论、理解语境。', source: 'Story Graph 方案讨论', quote: '阶段总结与聚合知识应保留来源,支持追溯原始记录。', related: 'k2', reason: '共同要求:聚合知识仍可回到原始记录' },
|
||||
{ id: 'k0', story: 'knowledge', stage: 0, x: 706, y: 213, label: '学习记录', title: '记录学习中的问题', copy: '学习时产生的笔记与问答散落在不同会话中。先保留它们的上下文,再考虑如何归入个人知识库。', source: '学习知识图谱讨论', quote: '学习时的提问和笔记可以放在一起,之后能够继续补充。' },
|
||||
{ id: 'k1', story: 'knowledge', stage: 1, x: 635, y: 304, label: '知识关联', title: '把同一个问题的线索连起来', copy: '不同笔记可能围绕同一个问题。关联需要说明它们共有的主题,以及各自提供了什么补充。', source: '个人知识库设计记录', quote: '关联不只是两段内容相似,还要说明这条联系为什么有用。' },
|
||||
{ id: 'k2', story: 'knowledge', stage: 2, x: 562, y: 240, label: '来源追溯', title: '知识条目携带出处', copy: '知识库中的归纳附带原始笔记和讨论位置。用户可以从结论返回上下文,判断它是否适用于当前问题。', source: '知识条目来源设计', quote: '每条归纳都应该能够展开出处,看到原话和产生结论时的上下文。', related: 'g2', reason: '共同要求:聚合知识仍可回到原始记录' },
|
||||
{ id: 'm0', story: 'memory', stage: 0, x: 380, y: 475, label: '原始数据', title: '从真实记录开始', copy: '原始会话、笔记和执行结果构成记忆的基础。上层归纳需要能指向这些记录。', source: '记忆基础讨论', quote: '先有记录,再有总结;总结不能取代原始记录。' },
|
||||
{ id: 'm1', story: 'memory', stage: 1, x: 465, y: 504, label: 'DIKW 分层', title: '区分记录与理解的层次', copy: '尝试按数据、信息、知识、智慧组织记忆。图谱中的阶段总结与知识聚合,可能成为这些层次之间的连接。', source: 'DIKW 设计草案', quote: '数据、信息、知识、智慧应当分层呈现,让用户知道当前看到的是什么。' },
|
||||
{ id: 'm2', story: 'memory', stage: 2, x: 478, y: 438, label: '可解释的归纳', title: '让每层归纳都能被解释', copy: '从数据到知识的每一步,都应说明归纳依据。阶段总结可以连接过程记录,聚合知识可以连接多个阶段。', source: '记忆分层讨论', quote: '一条知识来自哪些信息、信息又来自哪些数据,需要能够顺着关系找回去。', related: 'g2', reason: '共同要求:上层归纳保留底层依据' }
|
||||
];
|
||||
const dates = ['09.01 · 故事起点', '09.07 · 持续探索', '09.14 · 知识汇聚'];
|
||||
let activeStory = null;
|
||||
let selected = 'g2';
|
||||
let stage = 2;
|
||||
const graph = document.querySelector('#graph');
|
||||
const detail = document.querySelector('#detail');
|
||||
|
||||
function showDetail() {
|
||||
const node = nodes.find(item => item.id === selected);
|
||||
const story = stories.find(item => item.id === node.story);
|
||||
const related = node.related && nodes.find(item => item.id === node.related);
|
||||
detail.innerHTML = `<div class="detail-intro"><div class="detail-kicker">${story.title} / 阶段 ${node.stage + 1}</div><h2 class="detail-title">${node.title}</h2><p class="detail-copy">${node.copy}</p></div>
|
||||
<div class="detail-block"><h3>跨时间线关联</h3>${related && related.stage <= stage ? `<button class="relation-link" data-target="${related.id}">${related.title} ↗<small>${node.reason}</small></button>` : '<p class="muted">此阶段尚未建立跨线关联。</p>'}</div>
|
||||
<div class="detail-block"><h3>来源记录 · 1 条</h3><details class="source"><summary>${node.source}</summary><small>模拟会话 / 2026.${dates[node.stage].slice(0, 5)} / 消息 #${node.stage + 1}</small><p>“${node.quote}”</p></details><small>演示来源,可展开查看模拟原文。</small></div>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
document.querySelector('#timeline-list').innerHTML = stories.map(story => `<button class="timeline" data-story="${story.id}" aria-pressed="${activeStory === story.id}" style="--story-color:${story.color}"><span class="timeline-top"><i class="timeline-color"></i><span>0${stage + 1} 个阶段</span></span><strong>${story.title}</strong><small>${story.description}</small></button>`).join('');
|
||||
document.querySelector('#graph-title').textContent = activeStory ? stories.find(item => item.id === activeStory).title : '全部故事';
|
||||
document.querySelector('#node-count').textContent = ` / ${(stage + 1) * (activeStory ? 1 : 3)} 个阶段`;
|
||||
document.querySelector('#date-label').textContent = dates[stage];
|
||||
const showRelations = document.querySelector('#relations').checked;
|
||||
const flat = document.querySelector('#view').value === 'flat';
|
||||
document.querySelector('.sidebar-note p').innerHTML = flat ? '时间沿外圈逆时针推进。<br>内部平铺知识节点,<br>时间点直接连接对应节点。' : '一个圆,一条时间线。<br>圆内节点是阶段总结,<br>圆间虚线是知识关联。';
|
||||
document.querySelector('.canvas-caption').innerHTML = `<span class="live-dot"></span> ${flat ? '外圈时间逆时针推进 · 点击节点高亮时间连线' : '阶段总结构成故事,关联让知识相遇'}`;
|
||||
const plotted = nodes.map(node => flat ? { ...node, x: [280, 430, 580][stories.findIndex(story => story.id === node.story)], y: [210, 330, 450][node.stage] } : node);
|
||||
let markup = stories.map(story => `<g style="--story-color:${story.color}" class="${activeStory && activeStory !== story.id ? 'dim' : ''}"><circle class="orbit" cx="${story.cx}" cy="${story.cy}" r="${story.r}"/><circle class="orbit-inner" cx="${story.cx}" cy="${story.cy}" r="${story.r * .62}"/><text class="orbit-label" x="${story.cx}" y="${story.cy - story.r + 36}" text-anchor="middle">${story.title}</text><text class="orbit-caption" x="${story.cx}" y="${story.cy - story.r + 55}" text-anchor="middle">${stage + 1} 个阶段 · ${story.id === 'graph' ? '持续收敛' : story.id === 'knowledge' ? '多向探索' : '逐步归纳'}</text><path class="story-path" d="${nodes.filter(node => node.story === story.id && node.stage <= stage).map((node, index) => `${index ? 'L' : 'M'}${node.x},${node.y}`).join(' ')}"/></g>`).join('');
|
||||
if (showRelations && stage === 2) markup += '<path class="cross-path" d="M326 215 Q439 153 562 240 M326 215 Q366 362 478 438 M562 240 Q565 365 478 438"/><text class="bridge-label" x="413" y="188">来源追溯</text><text class="bridge-label" x="372" y="367">归纳依据</text>';
|
||||
if (flat) {
|
||||
const anchors = [{ x: 430, y: 65 }, { x: 95, y: 310 }, { x: 765, y: 310 }];
|
||||
markup = `<defs><marker id="time-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 Z" fill="var(--accent)"/></marker></defs>
|
||||
<ellipse class="time-boundary" cx="430" cy="310" rx="335" ry="245"/>
|
||||
<path class="time-ring" marker-end="url(#time-arrow)" d="M430 65 A335 245 0 0 0 95 310 A335 245 0 0 0 430 555 A335 245 0 0 0 765 310 A335 245 0 0 0 560 84"/>
|
||||
<text class="orbit-caption" x="430" y="30" text-anchor="middle">起点 · 逆时针沿外圈阅读 ↶</text>`;
|
||||
markup += plotted.filter(node => node.stage <= stage).map(node => {
|
||||
const anchor = anchors[node.stage];
|
||||
return `<path class="time-link ${node.id === selected ? 'active' : ''} ${activeStory && activeStory !== node.story ? 'dim' : ''}" d="M${anchor.x} ${anchor.y} Q${anchor.x} ${node.y} ${node.x} ${node.y}"/>`;
|
||||
}).join('');
|
||||
markup += stories.map(story => `<g style="--story-color:${story.color}" class="${activeStory && activeStory !== story.id ? 'dim' : ''}"><path class="story-path" d="${plotted.filter(node => node.story === story.id && node.stage <= stage).map((node, index) => `${index ? 'L' : 'M'}${node.x},${node.y}`).join(' ')}"/><text class="orbit-caption" text-anchor="middle" x="${plotted.find(node => node.story === story.id).x}" y="165">${story.title}</text></g>`).join('');
|
||||
if (showRelations && stage === 2) markup += '<path class="cross-path" d="M280 450 Q355 407 430 450 M280 450 Q430 515 580 450 M430 450 Q505 407 580 450"/>';
|
||||
markup += anchors.map((anchor, index) => `<g class="time-point ${index > stage ? 'dim' : ''}"><circle cx="${anchor.x}" cy="${anchor.y}" r="7"/><text x="${anchor.x}" y="${anchor.y - 20}" text-anchor="middle">${dates[index].slice(0, 5)}</text><text class="orbit-caption" x="${anchor.x}" y="${anchor.y + 28}" text-anchor="middle">${['首次提出', '持续补充', '形成归纳'][index]}</text></g>`).join('');
|
||||
}
|
||||
markup += plotted.filter(node => node.stage <= stage).map(node => {
|
||||
const story = stories.find(item => item.id === node.story);
|
||||
return `<g class="node ${node.id === selected ? 'selected' : ''} ${activeStory && activeStory !== node.story ? 'dim' : ''}" style="--story-color:${story.color}" transform="translate(${node.x} ${node.y})" tabindex="0" role="button" aria-label="${story.title}:${node.title}" aria-pressed="${node.id === selected}" data-node="${node.id}"><circle class="halo" r="22"/><circle class="core" r="${node.stage === 2 ? 10 : 7}"/><text text-anchor="middle" y="34">${node.label}</text></g>`;
|
||||
}).join('');
|
||||
graph.innerHTML = markup;
|
||||
showDetail();
|
||||
}
|
||||
|
||||
function selectNode(id) {
|
||||
selected = id;
|
||||
render();
|
||||
}
|
||||
document.querySelector('#timeline-list').addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-story]');
|
||||
if (!button) return;
|
||||
activeStory = activeStory === button.dataset.story ? null : button.dataset.story;
|
||||
if (activeStory) selected = nodes.find(node => node.story === activeStory && node.stage === stage).id;
|
||||
render();
|
||||
});
|
||||
graph.addEventListener('click', event => {
|
||||
const node = event.target.closest('[data-node]');
|
||||
if (node) selectNode(node.dataset.node);
|
||||
});
|
||||
graph.addEventListener('keydown', event => {
|
||||
const node = event.target.closest('[data-node]');
|
||||
if (node && (event.key === 'Enter' || event.key === ' ')) {
|
||||
event.preventDefault();
|
||||
const id = node.dataset.node;
|
||||
selectNode(id);
|
||||
graph.querySelector(`[data-node="${id}"]`).focus();
|
||||
}
|
||||
});
|
||||
detail.addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-target]');
|
||||
if (!button) return;
|
||||
activeStory = null;
|
||||
selectNode(button.dataset.target);
|
||||
});
|
||||
document.querySelector('#relations').addEventListener('change', render);
|
||||
document.querySelector('#view').addEventListener('change', render);
|
||||
document.querySelector('#reset').addEventListener('click', () => { activeStory = null; render(); });
|
||||
document.querySelector('#stage').addEventListener('input', event => {
|
||||
stage = Number(event.target.value);
|
||||
const story = activeStory || nodes.find(node => node.id === selected).story;
|
||||
selected = nodes.find(node => node.story === story && node.stage === stage).id;
|
||||
render();
|
||||
});
|
||||
document.querySelector('#theme').addEventListener('click', () => {
|
||||
document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
||||
});
|
||||
document.querySelector('#insight').addEventListener('click', () => {
|
||||
stage = 2;
|
||||
activeStory = null;
|
||||
document.querySelector('#stage').value = '2';
|
||||
document.querySelector('#relations').checked = true;
|
||||
render();
|
||||
detail.innerHTML = `<div class="detail-intro"><div class="detail-kicker">跨时间线聚合 / 示例</div><h2 class="detail-title">知识需要保留来路</h2><p class="detail-copy">三个故事指向同一个设计要求:不论是阶段总结、知识条目,还是分层记忆,归纳结果都需要连接原始依据。</p></div><div class="detail-block"><h3>来自 3 条时间线</h3>${['g2', 'k2', 'm2'].map(id => { const node = nodes.find(item => item.id === id); return `<button class="relation-link" data-target="${id}">${node.title} ↗<small>${stories.find(item => item.id === node.story).title} · 点击查看阶段与来源</small></button>`; }).join('')}</div><div class="detail-block"><h3>聚合说明</h3><p class="muted">以上为预置的聚合示例。实际功能需要结合来源生成关联依据,并允许用户核对。</p></div>`;
|
||||
document.querySelector('.inspector').scrollIntoView({ block: 'nearest' });
|
||||
});
|
||||
render();
|
||||
@@ -0,0 +1,39 @@
|
||||
# 故事线图谱产品与交互设计
|
||||
|
||||
日期:2026-09-16。状态:目标设计;当前 Demo 仅验证部分布局与交互。
|
||||
|
||||
图谱属于[监督者应用](../conversation-supervision/supervisor-prd.md),负责展示工作过程与知识演变。应用入口、心跳和知识库写入边界由该产品设计定义。
|
||||
|
||||
## 对象
|
||||
|
||||
对象之间的关系与监督者数据流见[逻辑设计](../conversation-supervision/logic-design.md),本文负责对象含义及用户操作要求。
|
||||
|
||||
FR-G1:外圈时间轴承载事件,内部节点表示持续积累的知识实体,阶段总结解释一个时间区间。同一知识实体可连接多个时间事件,分别表示提出、补充、验证或修订。新增方案不自动意味着旧方案已失效。
|
||||
|
||||
| 对象 | 内容 |
|
||||
| --- | --- |
|
||||
| 故事线 | 围绕某个工作主题形成的事件与知识集合 |
|
||||
| 时间事件 | 发生时间、所属故事、来源及影响的知识实体 |
|
||||
| 知识实体 | 当前描述、关联来源、演变记录与确认状态 |
|
||||
| 阶段总结 | 指定范围内的决定、变化和未解决事项 |
|
||||
| 关系 | 两端对象、关系类型、依据与确认状态 |
|
||||
|
||||
## 环绕时间轴
|
||||
|
||||
FR-G2:默认平铺视图将时间轴放在外围,逆时针围绕内部知识实体。起点、终点、方向和当前时间范围持续可见,外圈保留缺口,避免暗示时间循环。日期密集时聚合事件数量,展开后选择具体事件;时间位置采用真实比例还是等距事件布局需在下一轮原型中确定并明确标注。
|
||||
|
||||
选择事件后,高亮其影响的知识实体,右侧显示事件摘要和原始记录入口。外围时间点与内部实体使用实际连线;未选中的关系降低显著度。支持缩放、平移和聚焦,新增内容与回放尽量保留实体位置。
|
||||
|
||||
FR-G3:选择知识实体时,高亮对应的多个时间点与连线,标明提出、补充、验证或修订。右侧优先显示当前理解,再列演变和来源。历史回放下显示截至所选时刻的理解。
|
||||
|
||||
FR-G4:选择关系后,展示类型、关联理由及两端依据。跨故事关系与时间到实体的关系分别说明。用户可以确认、修改类型或移除关系;自动发现的关联标注待核对。
|
||||
|
||||
FR-G5:支持选择时间区间查看阶段总结,通过底部控制回放已发生事件及当时已知内容。圆形视图保留,用于观察独立故事聚合;视图切换保留范围、选择与回放时刻。集中度只描述关联分布,不等同于效率或产出,具体计算方式尚待设计。
|
||||
|
||||
FR-G6:默认展示主要实体及阶段事件,选中后展开一层关联,进一步展开由用户触发。提供文字列表完成与图形等价的对象选择、关系查看和来源跳转,遵循[监督者 UI 设计](../conversation-supervision/ui-design.md)的键盘与窄屏要求。
|
||||
|
||||
## 原型边界与下一轮验证
|
||||
|
||||
当前 Demo 使用阶段节点、固定坐标和预置关联,支持环绕时间轴、视图切换、回放、来源展开及模拟聚合;未实现真实实体合并、生成、历史版本、知识库接入或继续讨论。
|
||||
|
||||
下一轮场景:用户查看近期总结,选择“故事图谱布局”实体,看到三次讨论分别提出圆形、增加平铺、改为外圈逆时针时间轴,再查看来源并预览继续讨论的上下文。该场景应验证同一实体的多时点连接、事件和关系详情、阶段选择及上下文预览。验收映射见[User Stories](../conversation-supervision/user-stories.md) US-S04 至 US-S13。
|
||||
Reference in New Issue
Block a user