@co-engram/viewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/dist/brand-logos.d.ts +9 -0
- package/dist/brand-logos.d.ts.map +1 -0
- package/dist/brand-logos.js +10 -0
- package/dist/brand-logos.js.map +1 -0
- package/dist/html.d.ts +21 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +299 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/app.d.ts +11 -0
- package/dist/runtime/app.d.ts.map +1 -0
- package/dist/runtime/app.js +437 -0
- package/dist/runtime/app.js.map +1 -0
- package/dist/runtime/decay.d.ts +16 -0
- package/dist/runtime/decay.d.ts.map +1 -0
- package/dist/runtime/decay.js +108 -0
- package/dist/runtime/decay.js.map +1 -0
- package/dist/runtime/graph.d.ts +13 -0
- package/dist/runtime/graph.d.ts.map +1 -0
- package/dist/runtime/graph.js +313 -0
- package/dist/runtime/graph.js.map +1 -0
- package/dist/runtime/i18n.d.ts +16 -0
- package/dist/runtime/i18n.d.ts.map +1 -0
- package/dist/runtime/i18n.js +76 -0
- package/dist/runtime/i18n.js.map +1 -0
- package/dist/runtime/tabs.d.ts +8 -0
- package/dist/runtime/tabs.d.ts.map +1 -0
- package/dist/runtime/tabs.js +1783 -0
- package/dist/runtime/tabs.js.map +1 -0
- package/dist/server.d.ts +73 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +985 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.d.ts +13 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +1632 -0
- package/dist/styles.js.map +1 -0
- package/dist/vendor/dompurify-source.d.ts +11 -0
- package/dist/vendor/dompurify-source.d.ts.map +1 -0
- package/dist/vendor/dompurify-source.js +15 -0
- package/dist/vendor/dompurify-source.js.map +1 -0
- package/dist/vendor/marked-source.d.ts +11 -0
- package/dist/vendor/marked-source.d.ts.map +1 -0
- package/dist/vendor/marked-source.js +18 -0
- package/dist/vendor/marked-source.js.map +1 -0
- package/dist/vendor/vis-network-source.d.ts +11 -0
- package/dist/vendor/vis-network-source.d.ts.map +1 -0
- package/dist/vendor/vis-network-source.js +46 -0
- package/dist/vendor/vis-network-source.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewer v2 runtime — 通用前端工具:本地 state、auth、fetch、
|
|
3
|
+
* 颜色映射、时间格式化、tab 切换、drawer 管理。
|
|
4
|
+
*
|
|
5
|
+
* 这里 export 的字符串会被嵌入到 SPA HTML 的 <script> 标签内,
|
|
6
|
+
* 在浏览器端执行。完全离线,不依赖任何外部 CDN。
|
|
7
|
+
*
|
|
8
|
+
* @module @co-engram/claude-code/viewer/runtime/app
|
|
9
|
+
*/
|
|
10
|
+
export const APP_RUNTIME = `
|
|
11
|
+
// ============================================================
|
|
12
|
+
// Co-Engram Viewer v2 runtime(纯 JS,无 Alpine/htmx 依赖)
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
const CO_ENGRAM_STATE = {
|
|
16
|
+
tab: 'stats',
|
|
17
|
+
token: ''
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const CO_ENGRAM = (function() {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
// === 颜色 / 类型 映射 ===
|
|
24
|
+
const SYNAPSE_FAMILY = {
|
|
25
|
+
extends: 'structural', part_of: 'structural', similar_to: 'structural',
|
|
26
|
+
depends_on: 'causal', causes: 'causal', follows: 'causal',
|
|
27
|
+
derives_from: 'evidential', contradicts: 'evidential', exemplifies: 'evidential',
|
|
28
|
+
supersedes: 'temporal', consolidates: 'temporal',
|
|
29
|
+
contextualizes: 'modulatory'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const FAMILY_COLOR = {
|
|
33
|
+
structural: '#3b82f6',
|
|
34
|
+
causal: '#f97316',
|
|
35
|
+
evidential: '#10b981',
|
|
36
|
+
temporal: '#8b5cf6',
|
|
37
|
+
modulatory: '#6b7280'
|
|
38
|
+
};
|
|
39
|
+
const CONTRADICTS_COLOR = '#ef4444';
|
|
40
|
+
|
|
41
|
+
const KIND_COLOR = {
|
|
42
|
+
fact: '#10b981',
|
|
43
|
+
observation: '#3b82f6',
|
|
44
|
+
pattern: '#8b5cf6',
|
|
45
|
+
procedure: '#f97316',
|
|
46
|
+
hypothesis: '#ef4444'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// 12 种 synapse kind 各自独立的颜色(同族保持色调相近,但明度不同以便区分)
|
|
50
|
+
const SYNAPSE_KIND_COLOR = {
|
|
51
|
+
// 结构族 · 蓝色系
|
|
52
|
+
extends: '#3b82f6', // 主蓝
|
|
53
|
+
part_of: '#60a5fa', // 浅蓝
|
|
54
|
+
similar_to: '#1e40af', // 深蓝
|
|
55
|
+
// 因果族 · 橙色系
|
|
56
|
+
depends_on: '#f97316', // 主橙
|
|
57
|
+
causes: '#fb923c', // 浅橙
|
|
58
|
+
follows: '#c2410c', // 深橙
|
|
59
|
+
// 证据族 · 绿色系(contradicts 独立红色)
|
|
60
|
+
derives_from: '#10b981', // 主绿
|
|
61
|
+
exemplifies: '#6ee7b7', // 浅绿
|
|
62
|
+
contradicts: '#ef4444', // 红(高优先级)
|
|
63
|
+
// 时间族 · 紫色系
|
|
64
|
+
supersedes: '#8b5cf6', // 主紫
|
|
65
|
+
consolidates: '#c4b5fd', // 浅紫
|
|
66
|
+
// 调节族 · 灰色系
|
|
67
|
+
contextualizes: '#6b7280' // 灰
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// === 术语提示(鼠标悬停时显示) ===
|
|
71
|
+
// key 命名约定:{category}.{value},与 CO_ENGRAM_LABELS 对齐。
|
|
72
|
+
const TOOLTIPS = {
|
|
73
|
+
kind: {
|
|
74
|
+
fact: '事实 (fact):被确认成立、可独立验证的客观陈述。例:"项目使用 PostgreSQL 14"。',
|
|
75
|
+
observation: '观察 (observation):一次性感知到的事实,可能尚未沉淀为稳定结论。例:"今天 CI 跑了 12 分钟"。',
|
|
76
|
+
pattern: '模式 (pattern):从多次观察归纳出的规律,可预测未来行为。例:"每周一早上构建时间会变长"。',
|
|
77
|
+
procedure: '流程 (procedure):步骤序列,执行后可复现某结果。例:"发布前需跑 pnpm check"。',
|
|
78
|
+
hypothesis: '假设 (hypothesis):待验证的猜测;在反例出现前可作工作假设。例:"慢查询可能源于缺失索引"。'
|
|
79
|
+
},
|
|
80
|
+
status: {
|
|
81
|
+
active: '活跃 (active):近期被检索或强化,在召回池中权重高。',
|
|
82
|
+
dormant: '休眠 (dormant):长期未被检索,权重已衰减但未遗忘。',
|
|
83
|
+
forgotten: '已遗忘 (forgotten):维护阶段主动遗忘,文件仍在但默认不召回。',
|
|
84
|
+
archived: '已归档 (archived):冷归档状态,仅用于历史回溯。'
|
|
85
|
+
},
|
|
86
|
+
freshness: {
|
|
87
|
+
fresh: '鲜活 (fresh):ageDays ≤ halfLife,最近被有效强化过,在召回池中权重最高。',
|
|
88
|
+
aging: '渐衰 (aging):halfLife < ageDays ≤ halfLife×2,权重正在下降,建议尽快强化。',
|
|
89
|
+
stale: '过时 (stale):halfLife×2 < ageDays ≤ halfLife×4,长期未强化,候选遗忘对象。',
|
|
90
|
+
forgotten: '遗忘 (forgotten):ageDays > halfLife×4,默认移出召回池(文件保留,Git 可追溯)。'
|
|
91
|
+
},
|
|
92
|
+
visibility: {
|
|
93
|
+
public: '公开 (public):所有人/所有 agent 可见。',
|
|
94
|
+
team: '团队 (team):仅同团队可见。',
|
|
95
|
+
private: '私有 (private):仅创建者可见。',
|
|
96
|
+
restricted: '受限 (restricted):需特定权限才能查看。'
|
|
97
|
+
},
|
|
98
|
+
synapse: {
|
|
99
|
+
extends: '扩展 (extends) · 结构族:A 在 B 基础上扩展,继承 B 的语义并新增维度。',
|
|
100
|
+
part_of: '部分 (part_of) · 结构族:A 是 B 的组成部分(B has-a A)。',
|
|
101
|
+
similar_to: '相似 (similar_to) · 结构族:A 与 B 语义相近,可互换或互援。',
|
|
102
|
+
depends_on: '依赖 (depends_on) · 因果族:A 的成立依赖 B(B 是 A 的前置条件)。',
|
|
103
|
+
causes: '导致 (causes) · 因果族:A 触发或产生 B(正向因果)。',
|
|
104
|
+
follows: '顺承 (follows) · 因果族:A 在时间/逻辑上跟随 B(无强因果)。',
|
|
105
|
+
derives_from: '派生 (derives_from) · 证据族:A 从 B 推导而来(B 是依据)。',
|
|
106
|
+
contradicts: '矛盾 (contradicts) · 证据族:A 与 B 相互冲突,进入裁决流程。',
|
|
107
|
+
exemplifies: '例证 (exemplifies) · 证据族:A 是 B 的具体实例/样本。',
|
|
108
|
+
supersedes: '取代 (supersedes) · 时间族:A 取代过时的 B(版本更迭)。',
|
|
109
|
+
consolidates: '整合 (consolidates) · 时间族:A 合并/精炼了 B 的内容。',
|
|
110
|
+
contextualizes: '上下文 (contextualizes) · 调节族:A 为 B 提供情境背景(非因果、非证据)。'
|
|
111
|
+
},
|
|
112
|
+
family: {
|
|
113
|
+
structural: '结构族 (structural):描述知识间的组成/扩展关系。蓝色。',
|
|
114
|
+
causal: '因果族 (causal):描述触发/依赖关系。橙色。',
|
|
115
|
+
evidential: '证据族 (evidential):描述来源/冲突关系。绿色(矛盾单独标红)。',
|
|
116
|
+
temporal: '时间族 (temporal):描述版本/演化关系。紫色。',
|
|
117
|
+
modulatory: '调节族 (modulatory):描述情境上下文关系。灰色。'
|
|
118
|
+
},
|
|
119
|
+
synapseDirection: {
|
|
120
|
+
directional: '单向 (directional):A → B,关系仅从源指向目标。',
|
|
121
|
+
bidirectional: '双向 (bidirectional):A ↔ B,关系对称适用。'
|
|
122
|
+
},
|
|
123
|
+
resolution: {
|
|
124
|
+
pending: '待处理 (pending):已检测到矛盾,等待裁决。',
|
|
125
|
+
auto_resolved: '已自动裁决 (auto_resolved):阶段 1,LLM 自动给出裁决。',
|
|
126
|
+
escalated: '已升级 (escalated):阶段 2,升级到归属人裁决。',
|
|
127
|
+
contested: '有争议 (contested):阶段 3,超时未响应,附警告。',
|
|
128
|
+
resolved: '已解决 (resolved):人工或自动最终结案。'
|
|
129
|
+
},
|
|
130
|
+
importance: '重要性 (importance):0-1 数值,越高在召回池中权重越大。由初始设置 + 强化信号 + 衰减综合得出。',
|
|
131
|
+
confidence: '置信度 (confidence):0-1 数值,反映该记忆成立的可信程度(与重要性独立)。',
|
|
132
|
+
retrievalCount: '检索次数 (retrievalCount):该记忆被搜索/召回命中的总次数。',
|
|
133
|
+
effectiveRetrievals: '有效检索 (effectiveRetrievals):命中后被实际采用(非过滤掉)的次数。',
|
|
134
|
+
failedUses: '失败使用 (failedUses):命中后被报告"无效/过时"的次数。失败过多会触发遗忘。',
|
|
135
|
+
reinforcementScore: '强化分数 (reinforcementScore):累计的正向强化信号。',
|
|
136
|
+
emotionalValence: {
|
|
137
|
+
positive: '积极 (positive):该记忆编码时带有正向情绪(成功/赞赏/解决)。强化权重略高。',
|
|
138
|
+
negative: '消极 (negative):该记忆编码时带有负向情绪(失败/警告/反驳)。用于警示未来决策。',
|
|
139
|
+
neutral: '中性 (neutral):编码时无明显情绪倾向,纯陈述性记忆。'
|
|
140
|
+
},
|
|
141
|
+
sourceType: {
|
|
142
|
+
firsthand: '一手 (firsthand):亲历/直接观测,可信度最高。',
|
|
143
|
+
secondhand: '二手 (secondhand):转述/文档/他人经验,需交叉验证。',
|
|
144
|
+
inferred: '推断 (inferred):从其他记忆归纳得出,无直接证据。'
|
|
145
|
+
},
|
|
146
|
+
verification: {
|
|
147
|
+
unverified: '未验证 (unverified):新创建,尚未通过元认知评分。',
|
|
148
|
+
plausible: '貌似成立 (plausible):overall ≥ 0.4,初步通过但仍有不确定性。',
|
|
149
|
+
probable: '较可能 (probable):overall ≥ 0.6,经多次检索未出现反例。',
|
|
150
|
+
verified: '已验证 (verified):overall ≥ 0.8 或人工确认,可作为决策依据。',
|
|
151
|
+
refuted: '已反驳 (refuted):出现强反例或元认知评分极低,不应再作依据。'
|
|
152
|
+
},
|
|
153
|
+
decayHalfLifeDays: '衰退半衰期 (decayHalfLifeDays):importance 每经过 N 天衰减一半。null 表示永不衰退。',
|
|
154
|
+
lastEffectiveAt: '最近一次有效 (lastEffectiveAt):该记忆最后一次被实际采纳/强化成功的时间戳。',
|
|
155
|
+
evidenceCount: '证据数量 (evidenceCount):支撑该记忆的独立证据条数(突触 + 元数据)。',
|
|
156
|
+
encodingContext: '记忆产生情境 (encodingContext):记忆创建时的背景描述,用于情境依赖回忆。',
|
|
157
|
+
perspective: '视角 (perspective):该记忆的观察视角标识(多视角保留机制,spec §5.3)。',
|
|
158
|
+
importanceVector: '多维重要性 (importanceVector):把 importance 拆解为 5 个独立维度,便于精细化调控。',
|
|
159
|
+
importanceDim: {
|
|
160
|
+
personal: '个人维度 (personal):对当前用户的工作关联度。',
|
|
161
|
+
team: '团队维度 (team):对整个团队的协作价值。',
|
|
162
|
+
project: '项目维度 (project):与当前项目目标的契合度。',
|
|
163
|
+
network: '网络维度 (network):基于突触连接数派生,反映知识图谱中心性。',
|
|
164
|
+
temporal: '时间维度 (temporal):基于 lastEffectiveAt + 半衰期派生,近期强化的得分高。'
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
function tip(key) {
|
|
169
|
+
// 支持 'kind.fact' / 'importance' 两种 key 形式
|
|
170
|
+
const parts = key.split('.');
|
|
171
|
+
let v = TOOLTIPS;
|
|
172
|
+
for (const p of parts) {
|
|
173
|
+
if (v && typeof v === 'object' && p in v) v = v[p];
|
|
174
|
+
else return '';
|
|
175
|
+
}
|
|
176
|
+
if (typeof v !== 'string') return '';
|
|
177
|
+
return ' title="' + v.replaceAll('"', '"') + '"';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function synapseFamily(kind) {
|
|
181
|
+
return SYNAPSE_FAMILY[kind] || 'modulatory';
|
|
182
|
+
}
|
|
183
|
+
function familyColor(family) {
|
|
184
|
+
return FAMILY_COLOR[family] || FAMILY_COLOR.modulatory;
|
|
185
|
+
}
|
|
186
|
+
function kindColor(kind) {
|
|
187
|
+
return KIND_COLOR[kind] || '#6b7280';
|
|
188
|
+
}
|
|
189
|
+
function edgeColor(kind) {
|
|
190
|
+
// 优先用 12 种独立颜色,让每种 kind 视觉上可区分
|
|
191
|
+
if (SYNAPSE_KIND_COLOR[kind]) return SYNAPSE_KIND_COLOR[kind];
|
|
192
|
+
return kind === 'contradicts' ? CONTRADICTS_COLOR : familyColor(synapseFamily(kind));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// === 时间格式化 ===
|
|
196
|
+
function relativeTime(iso) {
|
|
197
|
+
if (!iso) return '';
|
|
198
|
+
const then = new Date(iso).getTime();
|
|
199
|
+
if (Number.isNaN(then)) return iso;
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
const diffSec = Math.max(0, Math.floor((now - then) / 1000));
|
|
202
|
+
if (diffSec < 60) return diffSec + 's ago';
|
|
203
|
+
if (diffSec < 3600) return Math.floor(diffSec / 60) + 'm ago';
|
|
204
|
+
if (diffSec < 86400) return Math.floor(diffSec / 3600) + 'h ago';
|
|
205
|
+
if (diffSec < 86400 * 7) return Math.floor(diffSec / 86400) + 'd ago';
|
|
206
|
+
return new Date(iso).toLocaleDateString();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function escapeHtml(s) {
|
|
210
|
+
if (s == null) return '';
|
|
211
|
+
return String(s)
|
|
212
|
+
.replaceAll('&', '&')
|
|
213
|
+
.replaceAll('<', '<')
|
|
214
|
+
.replaceAll('>', '>')
|
|
215
|
+
.replaceAll('"', '"')
|
|
216
|
+
.replaceAll("'", ''');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function importanceBar(value) {
|
|
220
|
+
const v = Math.max(0, Math.min(1, Number(value) || 0));
|
|
221
|
+
const pct = Math.round(v * 100);
|
|
222
|
+
return '<span class="importance-bar" title="importance=' + pct + '%"><span style="width:' + pct + '%"></span></span>';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// === Markdown 渲染(用于 engram/synapse 内容显示)===
|
|
226
|
+
// marked.parse 把 markdown 转 HTML,DOMPurify.sanitize 消毒后返回。
|
|
227
|
+
// 限定 ALLOWED_TAGS 防止 XSS(engram 内容来自 LLM/用户,可能含恶意脚本)。
|
|
228
|
+
// 两个 vendor lib 都通过 vendor/*.ts inline,完全离线。
|
|
229
|
+
function renderMarkdown(md) {
|
|
230
|
+
if (md == null) return '';
|
|
231
|
+
var input = String(md);
|
|
232
|
+
if (input.trim().length === 0) return '';
|
|
233
|
+
if (typeof window.marked === 'undefined' || typeof window.DOMPurify === 'undefined') {
|
|
234
|
+
// vendor 未加载,降级为 escape 后纯文本
|
|
235
|
+
return '<p>' + escapeHtml(input) + '</p>';
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
var html = window.marked.parse(input, { breaks: true, gfm: true });
|
|
239
|
+
return window.DOMPurify.sanitize(html, {
|
|
240
|
+
ALLOWED_TAGS: [
|
|
241
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
242
|
+
'p', 'strong', 'em', 'del', 'code', 'pre',
|
|
243
|
+
'ul', 'ol', 'li', 'blockquote', 'a', 'hr', 'br',
|
|
244
|
+
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
245
|
+
'img', 'span'
|
|
246
|
+
],
|
|
247
|
+
ALLOWED_ATTR: ['href', 'title', 'src', 'alt'],
|
|
248
|
+
ALLOW_DATA_ATTR: false
|
|
249
|
+
});
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.error('[co-engram] markdown render failed:', e);
|
|
252
|
+
return '<p>' + escapeHtml(input) + '</p>';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// === Auth / fetch wrapper ===
|
|
257
|
+
function getToken() {
|
|
258
|
+
return CO_ENGRAM_STATE.token || '';
|
|
259
|
+
}
|
|
260
|
+
function setToken(v) {
|
|
261
|
+
CO_ENGRAM_STATE.token = v || '';
|
|
262
|
+
try { localStorage.setItem('co-engram-viewer-token', CO_ENGRAM_STATE.token); } catch {}
|
|
263
|
+
}
|
|
264
|
+
function loadToken() {
|
|
265
|
+
try { CO_ENGRAM_STATE.token = localStorage.getItem('co-engram-viewer-token') || ''; } catch {}
|
|
266
|
+
}
|
|
267
|
+
function authHeaders() {
|
|
268
|
+
const t = getToken();
|
|
269
|
+
return t ? { Authorization: 'Bearer ' + t } : {};
|
|
270
|
+
}
|
|
271
|
+
async function apiGet(url) {
|
|
272
|
+
const r = await fetch(url, { headers: Object.assign({}, authHeaders(), { Accept: 'application/json' }) });
|
|
273
|
+
if (!r.ok) throw new Error('GET ' + url + ' → ' + r.status);
|
|
274
|
+
return r.json();
|
|
275
|
+
}
|
|
276
|
+
async function apiJson(url, method, body) {
|
|
277
|
+
const r = await fetch(url, {
|
|
278
|
+
method: method,
|
|
279
|
+
headers: Object.assign({}, authHeaders(), { 'Content-Type': 'application/json' }),
|
|
280
|
+
body: body == null ? undefined : JSON.stringify(body)
|
|
281
|
+
});
|
|
282
|
+
if (!r.ok) throw new Error(method + ' ' + url + ' → ' + r.status);
|
|
283
|
+
return r.json().catch(function() { return {}; });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// === Tab 切换 ===
|
|
287
|
+
function showTab(name) {
|
|
288
|
+
CO_ENGRAM_STATE.tab = name;
|
|
289
|
+
document.querySelectorAll('.tab').forEach(function(b) {
|
|
290
|
+
b.classList.toggle('active', b.dataset.tab === name);
|
|
291
|
+
});
|
|
292
|
+
document.querySelectorAll('section.tab-panel').forEach(function(s) {
|
|
293
|
+
s.classList.toggle('active', s.dataset.tab === name);
|
|
294
|
+
});
|
|
295
|
+
CO_ENGRAM.onTabEnter(name);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// === Drawer (right side detail panel) ===
|
|
299
|
+
function openDrawer(html) {
|
|
300
|
+
const drawer = document.getElementById('detail-drawer');
|
|
301
|
+
if (!drawer) return;
|
|
302
|
+
drawer.querySelector('.drawer-body').innerHTML = html;
|
|
303
|
+
drawer.classList.add('open');
|
|
304
|
+
}
|
|
305
|
+
function closeDrawer() {
|
|
306
|
+
const drawer = document.getElementById('detail-drawer');
|
|
307
|
+
if (drawer) drawer.classList.remove('open');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// === Audit action 分类 ===
|
|
311
|
+
function auditActionClass(action) {
|
|
312
|
+
if (action === 'contradicted') return 'audit-contradicted';
|
|
313
|
+
if (action === 'retrieve_hit' || action === 'retrieve_effective' || action === 'retrieve_inconclusive') return 'audit-effective';
|
|
314
|
+
if (action === 'propose' || action === 'accept' || action === 'dismiss') return 'audit-proposal';
|
|
315
|
+
return 'audit-state';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// === 注册表 ===
|
|
319
|
+
const TAB_HANDLERS = {};
|
|
320
|
+
function on(name, fn) { TAB_HANDLERS[name] = fn; }
|
|
321
|
+
function onTabEnter(name) {
|
|
322
|
+
const h = TAB_HANDLERS[name];
|
|
323
|
+
if (h) {
|
|
324
|
+
try {
|
|
325
|
+
const result = h();
|
|
326
|
+
if (result && typeof result.catch === 'function') {
|
|
327
|
+
result.catch(function(e) {
|
|
328
|
+
console.error('[co-engram] tab handler async failed:', e);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
} catch (e) {
|
|
332
|
+
console.error('[co-engram] tab handler failed:', e);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 清空 stats tab 的搜索结果,回到默认统计视图
|
|
338
|
+
function clearSearch() {
|
|
339
|
+
var input = document.getElementById('search-input');
|
|
340
|
+
if (input) input.value = '';
|
|
341
|
+
var resultsEl = document.getElementById('search-results');
|
|
342
|
+
if (resultsEl) {
|
|
343
|
+
resultsEl.style.display = 'none';
|
|
344
|
+
resultsEl.innerHTML = '';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
SYNAPSE_FAMILY: SYNAPSE_FAMILY, FAMILY_COLOR: FAMILY_COLOR,
|
|
350
|
+
KIND_COLOR: KIND_COLOR, CONTRADICTS_COLOR: CONTRADICTS_COLOR,
|
|
351
|
+
SYNAPSE_KIND_COLOR: SYNAPSE_KIND_COLOR,
|
|
352
|
+
TOOLTIPS: TOOLTIPS,
|
|
353
|
+
synapseFamily: synapseFamily, familyColor: familyColor,
|
|
354
|
+
kindColor: kindColor, edgeColor: edgeColor,
|
|
355
|
+
relativeTime: relativeTime, escapeHtml: escapeHtml, importanceBar: importanceBar,
|
|
356
|
+
renderMarkdown: renderMarkdown,
|
|
357
|
+
tip: tip,
|
|
358
|
+
getToken: getToken, setToken: setToken, loadToken: loadToken,
|
|
359
|
+
authHeaders: authHeaders, apiGet: apiGet, apiJson: apiJson,
|
|
360
|
+
showTab: showTab, openDrawer: openDrawer, closeDrawer: closeDrawer,
|
|
361
|
+
clearSearch: clearSearch,
|
|
362
|
+
auditActionClass: auditActionClass,
|
|
363
|
+
on: on, onTabEnter: onTabEnter
|
|
364
|
+
};
|
|
365
|
+
})();
|
|
366
|
+
|
|
367
|
+
window.CO_ENGRAM = CO_ENGRAM;
|
|
368
|
+
|
|
369
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
370
|
+
// 加载已保存的 token
|
|
371
|
+
CO_ENGRAM.loadToken();
|
|
372
|
+
// 同步到 token-input(如果存在)
|
|
373
|
+
var tokenInput = document.getElementById('token-input');
|
|
374
|
+
if (tokenInput) {
|
|
375
|
+
tokenInput.value = CO_ENGRAM.getToken();
|
|
376
|
+
tokenInput.addEventListener('input', function(ev) {
|
|
377
|
+
CO_ENGRAM.setToken(ev.target.value);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// tab 点击切换
|
|
382
|
+
document.querySelectorAll('.tab').forEach(function(btn) {
|
|
383
|
+
btn.addEventListener('click', function() { CO_ENGRAM.showTab(btn.dataset.tab); });
|
|
384
|
+
});
|
|
385
|
+
// 默认显示 stats
|
|
386
|
+
CO_ENGRAM.showTab('stats');
|
|
387
|
+
|
|
388
|
+
// 搜索栏:ID 绑定(仅在 stats tab 内)
|
|
389
|
+
var searchForm = document.getElementById('search-form');
|
|
390
|
+
if (searchForm) {
|
|
391
|
+
searchForm.addEventListener('submit', function(ev) {
|
|
392
|
+
ev.preventDefault();
|
|
393
|
+
var input = document.getElementById('search-input');
|
|
394
|
+
var q = (input && input.value) ? input.value.trim() : '';
|
|
395
|
+
if (!q) return;
|
|
396
|
+
var resultsEl = document.getElementById('search-results');
|
|
397
|
+
if (!resultsEl) return;
|
|
398
|
+
resultsEl.style.display = 'block';
|
|
399
|
+
resultsEl.innerHTML = '<div class="empty">Searching...</div>';
|
|
400
|
+
fetch('/api/search?q=' + encodeURIComponent(q), { headers: CO_ENGRAM.authHeaders() })
|
|
401
|
+
.then(function(r) { return r.json(); })
|
|
402
|
+
.then(function(data) {
|
|
403
|
+
if (!data.results || data.results.length === 0) {
|
|
404
|
+
resultsEl.innerHTML = '<div class="empty">无匹配结果</div>';
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
var L = window.CO_ENGRAM_LABELS || {};
|
|
408
|
+
var kindLabelMap = L.kind || {};
|
|
409
|
+
resultsEl.innerHTML = '<div class="grid cols-3">' + data.results.map(function(r) {
|
|
410
|
+
var e = r.entry || r.engram || r;
|
|
411
|
+
var id = CO_ENGRAM.escapeHtml(e.id || '');
|
|
412
|
+
var title = CO_ENGRAM.escapeHtml(e.title || e.id || '');
|
|
413
|
+
var kind = e.kind || '';
|
|
414
|
+
var kindLabel = kindLabelMap[kind] || kind || '—';
|
|
415
|
+
var kindTip = CO_ENGRAM.tip ? CO_ENGRAM.tip('kind.' + kind) : '';
|
|
416
|
+
return '<div class="card">'
|
|
417
|
+
+ '<div class="card-title" onclick="CO_ENGRAM_ENGRAMS.open(\\'' + id + '\\')">' + title + '</div>'
|
|
418
|
+
+ '<div><span class="chip kind-' + kind + '"' + kindTip + '>' + CO_ENGRAM.escapeHtml(kindLabel) + '</span></div>'
|
|
419
|
+
+ '</div>';
|
|
420
|
+
}).join('') + '</div>';
|
|
421
|
+
})
|
|
422
|
+
.catch(function(err) {
|
|
423
|
+
resultsEl.innerHTML = '<div class="empty">Search failed: ' + CO_ENGRAM.escapeHtml(String(err.message || err)) + '</div>';
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// drawer close
|
|
429
|
+
var drawerClose = document.querySelector('#detail-drawer .drawer-close');
|
|
430
|
+
if (drawerClose) drawerClose.addEventListener('click', function() { CO_ENGRAM.closeDrawer(); });
|
|
431
|
+
// ESC 关 drawer
|
|
432
|
+
document.addEventListener('keydown', function(e) {
|
|
433
|
+
if (e.key === 'Escape') CO_ENGRAM.closeDrawer();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
`;
|
|
437
|
+
//# sourceMappingURL=app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/runtime/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0a1B,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewer runtime decay visualizer
|
|
3
|
+
*
|
|
4
|
+
* 浏览器端衰退状态计算 + 进度条 DOM 生成。
|
|
5
|
+
* 镜像 core/src/lifecycle/freshness.ts:30 的 computeFreshness 阈值:
|
|
6
|
+
* ageDays <= halfLife -> fresh
|
|
7
|
+
* ageDays <= halfLife * 2 -> aging
|
|
8
|
+
* ageDays <= halfLife * 4 -> stale
|
|
9
|
+
* ageDays > halfLife * 4 -> forgotten
|
|
10
|
+
*
|
|
11
|
+
* 进度条分母用 halfLife*4(完整衰退周期),让用户直观看到"还能撑多久才 forgotten"。
|
|
12
|
+
*
|
|
13
|
+
* @module @co-engram/viewer/runtime/decay
|
|
14
|
+
*/
|
|
15
|
+
export declare const DECAY_RUNTIME = "\n// ============================================================\n// Co-Engram Viewer decay visualizer\n// ============================================================\nwindow.CO_ENGRAM_DECAY = (function() {\n 'use strict';\n\n function computeDecayState(lastEffectiveAt, halfLifeDays, now) {\n const nowMs = (now || new Date()).getTime();\n\n // \u6C38\u4E0D\u8870\u9000\n if (halfLifeDays === null || halfLifeDays === undefined || halfLifeDays <= 0) {\n return null;\n }\n // \u4ECE\u672A\u751F\u6548\n if (!lastEffectiveAt) {\n return null;\n }\n\n const lastEffective = new Date(lastEffectiveAt).getTime();\n if (isNaN(lastEffective)) {\n return null;\n }\n\n let ageMs = nowMs - lastEffective;\n if (ageMs < 0) {\n // \u65F6\u949F\u504F\u5DEE(\u6781\u5C11\u89C1):\u89C6\u4E3A fresh,ageDays=0\n ageMs = 0;\n }\n const ageDays = ageMs / (1000 * 60 * 60 * 24);\n const halfLife = halfLifeDays;\n\n let currentLevel;\n let daysToNext;\n if (ageDays <= halfLife) {\n currentLevel = 'fresh';\n daysToNext = Math.max(0, Math.ceil(halfLife - ageDays));\n } else if (ageDays <= halfLife * 2) {\n currentLevel = 'aging';\n daysToNext = Math.max(0, Math.ceil(halfLife * 2 - ageDays));\n } else if (ageDays <= halfLife * 4) {\n currentLevel = 'stale';\n daysToNext = Math.max(0, Math.ceil(halfLife * 4 - ageDays));\n } else {\n currentLevel = 'forgotten';\n daysToNext = null;\n }\n\n // \u8FDB\u5EA6\u6761:0-100,\u57FA\u4E8E halfLife*4 \u4F5C\u5B8C\u6574\u5468\u671F\n const totalCycle = halfLife * 4;\n const progressPct = Math.max(0, Math.min(100, (ageDays / totalCycle) * 100));\n\n return {\n progressPct: progressPct,\n currentLevel: currentLevel,\n daysToNext: daysToNext,\n };\n }\n\n function renderDecayBar(decay, halfLifeDays) {\n const T = window.CO_ENGRAM_T;\n if (!decay) {\n // \u6C38\u4E0D\u8870\u9000 / \u4ECE\u672A\u751F\u6548 \u2014 \u52A0 title \u60AC\u505C\u89E3\u91CA\u6210\u56E0\n const isNeverDecays = (halfLifeDays === null || halfLifeDays === undefined || halfLifeDays <= 0);\n const msg = isNeverDecays\n ? T.t('decay.neverDecays')\n : T.t('decay.neverEffective');\n const tipKey = isNeverDecays ? 'decay.neverDecaysTip' : 'decay.neverEffectiveTip';\n const tipText = T.t(tipKey);\n const titleAttr = (tipText && tipText !== tipKey) ? ' title=\"' + tipText.replaceAll('\"', '"') + '\"' : '';\n return '<div class=\"decay-empty\"' + titleAttr + '>' + msg + '</div>';\n }\n\n const colorClass = 'freshness-' + decay.currentLevel;\n const countdown = T.decayLabel(decay.daysToNext);\n const levelText = T.enumLabel('freshness', decay.currentLevel);\n\n return ''\n + '<div class=\"decay-row\">'\n + '<span class=\"decay-level freshness-' + decay.currentLevel + '\">' + levelText + '</span>'\n + '<span class=\"decay-countdown\">' + countdown + '</span>'\n + '</div>'\n + '<div class=\"decay-bar\" title=\"' + T.t('field.label.decayProgress') + '\">'\n + '<div class=\"decay-fill ' + colorClass + '\" style=\"width:' + decay.progressPct.toFixed(1) + '%\"></div>'\n + '</div>';\n }\n\n return {\n computeDecayState: computeDecayState,\n renderDecayBar: renderDecayBar,\n };\n})();\n";
|
|
16
|
+
//# sourceMappingURL=decay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decay.d.ts","sourceRoot":"","sources":["../../src/runtime/decay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,aAAa,i0GA4FzB,CAAC"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewer runtime decay visualizer
|
|
3
|
+
*
|
|
4
|
+
* 浏览器端衰退状态计算 + 进度条 DOM 生成。
|
|
5
|
+
* 镜像 core/src/lifecycle/freshness.ts:30 的 computeFreshness 阈值:
|
|
6
|
+
* ageDays <= halfLife -> fresh
|
|
7
|
+
* ageDays <= halfLife * 2 -> aging
|
|
8
|
+
* ageDays <= halfLife * 4 -> stale
|
|
9
|
+
* ageDays > halfLife * 4 -> forgotten
|
|
10
|
+
*
|
|
11
|
+
* 进度条分母用 halfLife*4(完整衰退周期),让用户直观看到"还能撑多久才 forgotten"。
|
|
12
|
+
*
|
|
13
|
+
* @module @co-engram/viewer/runtime/decay
|
|
14
|
+
*/
|
|
15
|
+
export const DECAY_RUNTIME = `
|
|
16
|
+
// ============================================================
|
|
17
|
+
// Co-Engram Viewer decay visualizer
|
|
18
|
+
// ============================================================
|
|
19
|
+
window.CO_ENGRAM_DECAY = (function() {
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
function computeDecayState(lastEffectiveAt, halfLifeDays, now) {
|
|
23
|
+
const nowMs = (now || new Date()).getTime();
|
|
24
|
+
|
|
25
|
+
// 永不衰退
|
|
26
|
+
if (halfLifeDays === null || halfLifeDays === undefined || halfLifeDays <= 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
// 从未生效
|
|
30
|
+
if (!lastEffectiveAt) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const lastEffective = new Date(lastEffectiveAt).getTime();
|
|
35
|
+
if (isNaN(lastEffective)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let ageMs = nowMs - lastEffective;
|
|
40
|
+
if (ageMs < 0) {
|
|
41
|
+
// 时钟偏差(极少见):视为 fresh,ageDays=0
|
|
42
|
+
ageMs = 0;
|
|
43
|
+
}
|
|
44
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
45
|
+
const halfLife = halfLifeDays;
|
|
46
|
+
|
|
47
|
+
let currentLevel;
|
|
48
|
+
let daysToNext;
|
|
49
|
+
if (ageDays <= halfLife) {
|
|
50
|
+
currentLevel = 'fresh';
|
|
51
|
+
daysToNext = Math.max(0, Math.ceil(halfLife - ageDays));
|
|
52
|
+
} else if (ageDays <= halfLife * 2) {
|
|
53
|
+
currentLevel = 'aging';
|
|
54
|
+
daysToNext = Math.max(0, Math.ceil(halfLife * 2 - ageDays));
|
|
55
|
+
} else if (ageDays <= halfLife * 4) {
|
|
56
|
+
currentLevel = 'stale';
|
|
57
|
+
daysToNext = Math.max(0, Math.ceil(halfLife * 4 - ageDays));
|
|
58
|
+
} else {
|
|
59
|
+
currentLevel = 'forgotten';
|
|
60
|
+
daysToNext = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 进度条:0-100,基于 halfLife*4 作完整周期
|
|
64
|
+
const totalCycle = halfLife * 4;
|
|
65
|
+
const progressPct = Math.max(0, Math.min(100, (ageDays / totalCycle) * 100));
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
progressPct: progressPct,
|
|
69
|
+
currentLevel: currentLevel,
|
|
70
|
+
daysToNext: daysToNext,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderDecayBar(decay, halfLifeDays) {
|
|
75
|
+
const T = window.CO_ENGRAM_T;
|
|
76
|
+
if (!decay) {
|
|
77
|
+
// 永不衰退 / 从未生效 — 加 title 悬停解释成因
|
|
78
|
+
const isNeverDecays = (halfLifeDays === null || halfLifeDays === undefined || halfLifeDays <= 0);
|
|
79
|
+
const msg = isNeverDecays
|
|
80
|
+
? T.t('decay.neverDecays')
|
|
81
|
+
: T.t('decay.neverEffective');
|
|
82
|
+
const tipKey = isNeverDecays ? 'decay.neverDecaysTip' : 'decay.neverEffectiveTip';
|
|
83
|
+
const tipText = T.t(tipKey);
|
|
84
|
+
const titleAttr = (tipText && tipText !== tipKey) ? ' title="' + tipText.replaceAll('"', '"') + '"' : '';
|
|
85
|
+
return '<div class="decay-empty"' + titleAttr + '>' + msg + '</div>';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const colorClass = 'freshness-' + decay.currentLevel;
|
|
89
|
+
const countdown = T.decayLabel(decay.daysToNext);
|
|
90
|
+
const levelText = T.enumLabel('freshness', decay.currentLevel);
|
|
91
|
+
|
|
92
|
+
return ''
|
|
93
|
+
+ '<div class="decay-row">'
|
|
94
|
+
+ '<span class="decay-level freshness-' + decay.currentLevel + '">' + levelText + '</span>'
|
|
95
|
+
+ '<span class="decay-countdown">' + countdown + '</span>'
|
|
96
|
+
+ '</div>'
|
|
97
|
+
+ '<div class="decay-bar" title="' + T.t('field.label.decayProgress') + '">'
|
|
98
|
+
+ '<div class="decay-fill ' + colorClass + '" style="width:' + decay.progressPct.toFixed(1) + '%"></div>'
|
|
99
|
+
+ '</div>';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
computeDecayState: computeDecayState,
|
|
104
|
+
renderDecayBar: renderDecayBar,
|
|
105
|
+
};
|
|
106
|
+
})();
|
|
107
|
+
`;
|
|
108
|
+
//# sourceMappingURL=decay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decay.js","sourceRoot":"","sources":["../../src/runtime/decay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4F5B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewer v2 runtime — Graph tab(记忆突触)。
|
|
3
|
+
*
|
|
4
|
+
* 用 vis-network(已内联到 window.vis)渲染力导向图谱。节点按 EngramKind 着色,
|
|
5
|
+
* 边按 SynapseFamily 着色,contradicts 边红色虚线 + resolutionStatus badge。
|
|
6
|
+
*
|
|
7
|
+
* 点击节点 → 节点详情(记忆印迹)
|
|
8
|
+
* 点击边 → 突触详情(可编辑/删除,通过 CO_ENGRAM_SYNAPSES)
|
|
9
|
+
*
|
|
10
|
+
* @module @co-engram/claude-code/viewer/runtime/graph
|
|
11
|
+
*/
|
|
12
|
+
export declare const GRAPH_RUNTIME = "\nCO_ENGRAM.on('graph', async function renderGraph() {\n const container = document.getElementById('graph-canvas');\n if (!container) return;\n try {\n await renderGraphInner(container);\n } catch (e) {\n console.error('[co-engram] graph render failed:', e);\n container.innerHTML = '<div class=\"empty\"><div class=\"icon\">\u26A0\uFE0F</div>\u6E32\u67D3\u5931\u8D25:' + CO_ENGRAM.escapeHtml(String(e && e.message || e)) + '</div>';\n }\n});\n\nasync function renderGraphInner(container) {\n if (typeof vis === 'undefined' || !vis.Network) {\n container.innerHTML = '<div class=\"empty\"><div class=\"icon\">\u26A0\uFE0F</div>vis-network \u52A0\u8F7D\u5931\u8D25</div>';\n return;\n }\n if (CO_ENGRAM._graphState && CO_ENGRAM._graphState.initialized) {\n // \u5DF2\u521D\u59CB\u5316,\u53EA refit\n setTimeout(() => CO_ENGRAM._graphState.network.fit(), 30);\n return;\n }\n\n let graph;\n try {\n graph = await CO_ENGRAM.apiGet('/api/graph');\n } catch (e) {\n container.innerHTML = '<div class=\"empty\"><div class=\"icon\">\u26A0\uFE0F</div>\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>';\n return;\n }\n\n if (!graph.nodes || graph.nodes.length === 0) {\n container.innerHTML = '<div class=\"empty\"><div class=\"icon\">\uD83D\uDD73\uFE0F</div>\u6682\u65E0\u8BB0\u5FC6\u5370\u8FF9</div>';\n return;\n }\n\n const L = CO_ENGRAM_LABELS;\n const FAMILY_LABEL = { structural: '\u7ED3\u6784', causal: '\u56E0\u679C', evidential: '\u8BC1\u636E', temporal: '\u65F6\u95F4', modulatory: '\u8C03\u8282' };\n\n // === \u72B6\u6001:filter ===\n // showKinds: engram \u7C7B\u578B(\u8282\u70B9\u8FC7\u6EE4);showSynapseKinds: synapse \u7C7B\u578B(\u8FB9\u8FC7\u6EE4,\u4E0E\u7F16\u8F91\u5668\u4E00\u81F4)\n const ALL_SYNAPSE_KINDS = ['extends', 'part_of', 'similar_to', 'depends_on', 'causes', 'follows', 'derives_from', 'contradicts', 'exemplifies', 'supersedes', 'consolidates', 'contextualizes'];\n const state = {\n showKinds: { fact: true, observation: true, pattern: true, procedure: true, hypothesis: true },\n showSynapseKinds: Object.fromEntries(ALL_SYNAPSE_KINDS.map(k => [k, true])),\n physicsEnabled: true\n };\n CO_ENGRAM._graphState = { initialized: false, network: null, data: graph, state };\n\n // === \u8282\u70B9 / \u8FB9\u6784\u5EFA ===\n function buildNodes() {\n return graph.nodes\n .filter(n => state.showKinds[n.kind] !== false)\n .map(n => {\n const importance = (n.importance != null ? n.importance : 0.5);\n const size = 10 + importance * 18;\n const kindLabel = L.kind[n.kind] || n.kind;\n const kindTip = (CO_ENGRAM.TOOLTIPS && CO_ENGRAM.TOOLTIPS.kind && CO_ENGRAM.TOOLTIPS.kind[n.kind]) || '';\n const tipText = n.title + '\\n[' + kindLabel + ' / ' + n.kind + ']\\n\u6807\u7B7E:' + (n.domainTags || []).join(', ') + (kindTip ? '\\n\\n' + kindTip : '');\n return {\n id: n.id,\n label: n.title.length > 32 ? n.title.slice(0, 30) + '\u2026' : n.title,\n title: tipText,\n group: n.kind,\n color: {\n background: CO_ENGRAM.kindColor(n.kind),\n border: CO_ENGRAM.kindColor(n.kind),\n highlight: { background: CO_ENGRAM.kindColor(n.kind), border: '#000' },\n hover: { background: CO_ENGRAM.kindColor(n.kind), border: '#fff' }\n },\n size,\n font: { color: getComputedStyle(document.body).color, size: 11, face: 'sans-serif' },\n shape: 'dot',\n _raw: n\n };\n });\n }\n\n function buildEdges() {\n const out = [];\n for (const e of graph.edges) {\n // \u6309 12 kind \u8FC7\u6EE4(\u4E0E\u7F16\u8F91\u5668\u4E00\u81F4)\n if (state.showSynapseKinds[e.kind] === false) continue;\n\n const isContra = e.kind === 'contradicts';\n const color = CO_ENGRAM.edgeColor(e.kind);\n const kindLabel = L.synapse[e.kind] || e.kind;\n const family = CO_ENGRAM.synapseFamily(e.kind);\n const familyLabel = FAMILY_LABEL[family] || family;\n const synTip = (CO_ENGRAM.TOOLTIPS && CO_ENGRAM.TOOLTIPS.synapse && CO_ENGRAM.TOOLTIPS.synapse[e.kind]) || '';\n const resLabel = e.resolutionStatus ? ' \u00B7 \u88C1\u51B3:' + (L.resolution[e.resolutionStatus] || e.resolutionStatus) : '';\n const tipText = kindLabel + ' (' + e.kind + ') \u00B7 ' + familyLabel + '\u65CF\\n\u6743\u91CD ' + (e.weight != null ? e.weight.toFixed(2) : '?') + ' \u00B7 \u8BC1\u636E ' + (e.evidenceCount || 0) + resLabel + '\\n\u65B9\u5411:' + (e.direction || 'directional') + (synTip ? '\\n\\n' + synTip : '') + '\\n\\n[\u70B9\u51FB\u7F16\u8F91\u6B64\u7A81\u89E6]';\n out.push({\n id: e.id,\n from: e.from,\n to: e.to,\n label: '',\n title: tipText,\n color: { color, highlight: color, hover: color, opacity: 0.85 },\n width: 1 + (e.weight || 0.5) * 3,\n dashes: isContra,\n arrows: e.direction === 'bidirectional' ? { to: { enabled: true }, from: { enabled: true } } : { to: { enabled: true, scaleFactor: 0.6 } },\n smooth: { enabled: true, type: 'continuous', roundness: 0.5 },\n _raw: e\n });\n }\n return out;\n }\n\n // === \u521D\u59CB\u5316 vis-network ===\n const nodesDataset = new vis.DataSet(buildNodes());\n const edgesDataset = new vis.DataSet(buildEdges());\n\n const options = {\n autoResize: true,\n height: '100%',\n width: '100%',\n nodes: { borderWidth: 2, shadow: { enabled: true, size: 6, x: 0, y: 1 } },\n edges: { smooth: { type: 'continuous' } },\n physics: {\n enabled: true,\n solver: 'forceAtlas2Based',\n forceAtlas2Based: { gravitationalConstant: -65, centralGravity: 0.01, springLength: 110, springConstant: 0.08, damping: 0.4, avoidOverlap: 0.5 },\n stabilization: { iterations: 80, fit: true }\n },\n interaction: { hover: true, tooltipDelay: 100, navigationButtons: false, keyboard: false, selectConnectedEdges: false }\n };\n\n const network = new vis.Network(container, { nodes: nodesDataset, edges: edgesDataset }, options);\n CO_ENGRAM._graphState.network = network;\n CO_ENGRAM._graphState.initialized = true;\n CO_ENGRAM._graphState.nodes = nodesDataset;\n CO_ENGRAM._graphState.edges = edgesDataset;\n\n // === \u4EA4\u4E92 ===\n network.on('click', (params) => {\n // \u4F18\u5148\u5904\u7406\u8FB9\u70B9\u51FB(\u7A81\u89E6\u8BE6\u60C5)\n if (params.edges && params.edges.length > 0 && (!params.nodes || params.nodes.length === 0)) {\n const edgeId = params.edges[0];\n const edge = edgesDataset.get(edgeId);\n if (edge && edge._raw) {\n CO_ENGRAM_SYNAPSES.open(edge._raw.id);\n }\n return;\n }\n if (!params.nodes || params.nodes.length === 0) {\n // \u70B9\u7A7A\u767D:\u53D6\u6D88\u9AD8\u4EAE\n resetHighlight();\n return;\n }\n const id = params.nodes[0];\n focusNode(id);\n });\n\n function resetHighlight() {\n const allNodes = nodesDataset.get();\n const allEdges = edgesDataset.get();\n nodesDataset.update(allNodes.map(n => ({ id: n.id, opacity: 1.0 })));\n edgesDataset.update(allEdges.map(e => ({ id: e.id, color: e.color })));\n }\n\n function focusNode(id) {\n const connectedNodeIds = new Set([id]);\n const connectedEdgeIds = new Set();\n for (const e of graph.edges) {\n if (e.from === id) { connectedNodeIds.add(e.to); connectedEdgeIds.add(e.id); }\n if (e.to === id) { connectedNodeIds.add(e.from); connectedEdgeIds.add(e.id); }\n }\n const allNodes = nodesDataset.get();\n const allEdges = edgesDataset.get();\n nodesDataset.update(allNodes.map(n => ({\n id: n.id,\n opacity: connectedNodeIds.has(n.id) ? 1.0 : 0.15\n })));\n edgesDataset.update(allEdges.map(e => ({\n id: e.id,\n opacity: connectedEdgeIds.has(e.id) ? 1.0 : 0.05\n })));\n showNodeDetail(id);\n }\n\n async function showNodeDetail(id) {\n let detail;\n try {\n detail = await CO_ENGRAM.apiGet('/api/engrams/' + encodeURIComponent(id));\n } catch (e) {\n CO_ENGRAM.openDrawer('<h2>' + CO_ENGRAM.escapeHtml(id) + '</h2><div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>');\n return;\n }\n\n // \u627E\u8BE5\u8282\u70B9\u7684\u6240\u6709 synapse\n const outgoing = graph.edges.filter(e => e.from === id);\n const incoming = graph.edges.filter(e => e.to === id);\n\n const familyGroup = (list, label) => {\n if (!list.length) return '';\n const grouped = {};\n for (const e of list) {\n const fam = CO_ENGRAM.synapseFamily(e.kind);\n (grouped[fam] = grouped[fam] || []).push(e);\n }\n let html = '<h3>' + label + ' (' + list.length + ')</h3>';\n for (const fam of Object.keys(grouped)) {\n html += '<div class=\"field\"><span class=\"chip dot\" style=\"color:' + CO_ENGRAM.familyColor(fam) + '\">' + (FAMILY_LABEL[fam] || fam) + '</span></div>';\n for (const e of grouped[fam]) {\n const other = e.from === id ? e.to : e.from;\n const kindLabel = L.synapse[e.kind] || e.kind;\n html += '<div class=\"field\" style=\"padding-left:0.5rem\">'\n + '<span class=\"chip synapse-link\" data-synapse-id=\"' + CO_ENGRAM.escapeHtml(e.id) + '\" style=\"background:' + CO_ENGRAM.edgeColor(e.kind) + '22;color:' + CO_ENGRAM.edgeColor(e.kind) + ';cursor:pointer\">' + kindLabel + '</span> '\n + '<span class=\"engram-link\" data-engram-id=\"' + CO_ENGRAM.escapeHtml(other) + '\">' + CO_ENGRAM.escapeHtml(other) + '</span>'\n + (e.resolutionStatus ? ' <span class=\"chip\" style=\"background:rgba(239,68,68,.15);color:#ef4444\">' + (L.resolution[e.resolutionStatus] || e.resolutionStatus) + '</span>' : '')\n + '</div>';\n }\n }\n return html;\n };\n\n const body = [\n '<div class=\"edit-banner\" style=\"display:flex;gap:.5rem;align-items:center\"><strong style=\"margin-right:auto\">\u8282\u70B9\u8BE6\u60C5</strong>'\n + '<button class=\"btn\" onclick=\"CO_ENGRAM.showTab(\\'engrams\\');setTimeout(()=>CO_ENGRAM_ENGRAMS.open(\\'' + CO_ENGRAM.escapeHtml(detail.id) + '\\'),50)\">\u5728\u8BB0\u5FC6\u5370\u8FF9\u4E2D\u7F16\u8F91</button>'\n + '</div>',\n '<h2>' + CO_ENGRAM.escapeHtml(detail.title || detail.id) + '</h2>',\n '<div class=\"field\"><span class=\"chip kind-' + detail.kind + '\">' + (L.kind[detail.kind] || detail.kind) + '</span> '\n + CO_ENGRAM.importanceBar(detail.importance) + ' <span class=\"kpi-sub\">\u91CD\u8981\u6027 ' + (detail.importance || 0).toFixed(2) + '</span></div>',\n '<div class=\"field\"><span class=\"field-label\">ID:</span><code>' + CO_ENGRAM.escapeHtml(detail.id) + '</code></div>',\n (detail.domainTags && detail.domainTags.length\n ? '<div class=\"field\"><span class=\"field-label\">\u9886\u57DF\u6807\u7B7E:</span>' + detail.domainTags.map(t => '<span class=\"chip\">' + CO_ENGRAM.escapeHtml(t) + '</span>').join(' ') + '</div>'\n : ''),\n (detail.summary ? '<h3>\u6458\u8981</h3><div class=\"field\">' + CO_ENGRAM.escapeHtml(detail.summary) + '</div>' : ''),\n '<h3>\u7EDF\u8BA1</h3>',\n '<div class=\"field\"><span class=\"field-label\">\u68C0\u7D22:</span>' + (detail.retrievalCount || 0)\n + ' <span class=\"field-label\">\u6709\u6548:</span>' + (detail.effectiveRetrievals || 0)\n + ' <span class=\"field-label\">\u5931\u8D25:</span>' + (detail.failedUses || 0) + '</div>',\n '<div class=\"field\"><span class=\"field-label\">\u521B\u5EFA\u8005:</span>' + CO_ENGRAM.escapeHtml(detail.createdBy || '')\n + ' <span class=\"field-label\">\u65F6\u95F4:</span>' + CO_ENGRAM.escapeHtml(detail.createdAt || '') + '</div>',\n familyGroup(outgoing, '\u53D1\u51FA\u7A81\u89E6'),\n familyGroup(incoming, '\u63A5\u6536\u7A81\u89E6')\n ].join('\\n');\n CO_ENGRAM.openDrawer(body);\n\n // drawer \u5185\u7684 engram / synapse \u94FE\u63A5\u70B9\u51FB \u2192 focus / open\n setTimeout(() => {\n document.querySelectorAll('#detail-drawer .engram-link').forEach(el => {\n el.onclick = () => {\n const targetId = el.getAttribute('data-engram-id');\n if (targetId) {\n network.focus(targetId, { scale: 1.2, animation: { duration: 400, easingFunction: 'easeInOutQuad' } });\n network.selectNodes([targetId]);\n focusNode(targetId);\n }\n };\n });\n document.querySelectorAll('#detail-drawer .synapse-link').forEach(el => {\n el.onclick = () => {\n const sid = el.getAttribute('data-synapse-id');\n if (sid) CO_ENGRAM_SYNAPSES.open(sid);\n };\n });\n }, 50);\n }\n\n // === \u5DE5\u5177\u680F\u4EA4\u4E92 ===\n CO_ENGRAM._graphState.applyFilters = function() {\n nodesDataset.clear();\n edgesDataset.clear();\n nodesDataset.add(buildNodes());\n edgesDataset.add(buildEdges());\n };\n CO_ENGRAM._graphState.togglePhysics = function() {\n state.physicsEnabled = !state.physicsEnabled;\n network.setOptions({ physics: { enabled: state.physicsEnabled } });\n };\n CO_ENGRAM._graphState.fit = function() { network.fit({ animation: true }); };\n CO_ENGRAM._graphState.resetView = function() {\n state.showKinds = { fact: true, observation: true, pattern: true, procedure: true, hypothesis: true };\n state.showSynapseKinds = Object.fromEntries(ALL_SYNAPSE_KINDS.map(k => [k, true]));\n document.querySelectorAll('.graph-toolbar input[type=checkbox]').forEach(c => c.checked = true);\n CO_ENGRAM._graphState.applyFilters();\n network.fit({ animation: true });\n };\n}\n\n// \u5DE5\u5177\u680F\u70B9\u51FB\u5904\u7406(\u4ECE onclick \u8C03\u7528)\nwindow.CO_ENGRAM_GRAPH = {\n toggleSynapseKind(kind, checked) {\n const s = CO_ENGRAM._graphState;\n if (!s) return;\n s.state.showSynapseKinds[kind] = checked;\n s.applyFilters();\n },\n toggleKind(kind, checked) {\n const s = CO_ENGRAM._graphState;\n if (!s) return;\n s.state.showKinds[kind] = checked;\n s.applyFilters();\n },\n togglePhysics() { CO_ENGRAM._graphState && CO_ENGRAM._graphState.togglePhysics(); },\n fit() { CO_ENGRAM._graphState && CO_ENGRAM._graphState.fit(); },\n reset() { CO_ENGRAM._graphState && CO_ENGRAM._graphState.resetView(); }\n};\n";
|
|
13
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/runtime/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,aAAa,8kcA4SzB,CAAC"}
|