aifastdb-devplan 1.6.1 → 1.6.3
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/dist/dev-plan-document-store.d.ts +13 -1
- package/dist/dev-plan-document-store.d.ts.map +1 -1
- package/dist/dev-plan-document-store.js +119 -0
- package/dist/dev-plan-document-store.js.map +1 -1
- package/dist/dev-plan-factory.d.ts.map +1 -1
- package/dist/dev-plan-factory.js +3 -1
- package/dist/dev-plan-factory.js.map +1 -1
- package/dist/dev-plan-graph-store.d.ts +341 -9
- package/dist/dev-plan-graph-store.d.ts.map +1 -1
- package/dist/dev-plan-graph-store.js +2414 -210
- package/dist/dev-plan-graph-store.js.map +1 -1
- package/dist/dev-plan-interface.d.ts +119 -1
- package/dist/dev-plan-interface.d.ts.map +1 -1
- package/dist/dev-plan-migrate.d.ts +1 -0
- package/dist/dev-plan-migrate.d.ts.map +1 -1
- package/dist/dev-plan-migrate.js +28 -2
- package/dist/dev-plan-migrate.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/index.js +652 -0
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/shard-config.d.ts +64 -0
- package/dist/shard-config.d.ts.map +1 -0
- package/dist/shard-config.js +109 -0
- package/dist/shard-config.js.map +1 -0
- package/dist/types.d.ts +305 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/visualize/graph-canvas/api-compat.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/api-compat.js +22 -12
- package/dist/visualize/graph-canvas/api-compat.js.map +1 -1
- package/dist/visualize/graph-canvas/core.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/core.js +296 -4
- package/dist/visualize/graph-canvas/core.js.map +1 -1
- package/dist/visualize/graph-canvas/interaction.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/interaction.js +11 -0
- package/dist/visualize/graph-canvas/interaction.js.map +1 -1
- package/dist/visualize/graph-canvas/layout-worker.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/layout-worker.js +45 -9
- package/dist/visualize/graph-canvas/layout-worker.js.map +1 -1
- package/dist/visualize/graph-canvas/renderer.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/renderer.js +164 -33
- package/dist/visualize/graph-canvas/renderer.js.map +1 -1
- package/dist/visualize/graph-canvas/styles.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/styles.js +146 -121
- package/dist/visualize/graph-canvas/styles.js.map +1 -1
- package/dist/visualize/graph-canvas/viewport.d.ts.map +1 -1
- package/dist/visualize/graph-canvas/viewport.js +10 -0
- package/dist/visualize/graph-canvas/viewport.js.map +1 -1
- package/dist/visualize/server.js +371 -32
- package/dist/visualize/server.js.map +1 -1
- package/dist/visualize/template-core.d.ts +9 -0
- package/dist/visualize/template-core.d.ts.map +1 -0
- package/dist/visualize/template-core.js +721 -0
- package/dist/visualize/template-core.js.map +1 -0
- package/dist/visualize/template-data-loading.d.ts +7 -0
- package/dist/visualize/template-data-loading.d.ts.map +1 -0
- package/dist/visualize/template-data-loading.js +677 -0
- package/dist/visualize/template-data-loading.js.map +1 -0
- package/dist/visualize/template-detail-panel.d.ts +14 -0
- package/dist/visualize/template-detail-panel.d.ts.map +1 -0
- package/dist/visualize/template-detail-panel.js +624 -0
- package/dist/visualize/template-detail-panel.js.map +1 -0
- package/dist/visualize/template-graph-3d.d.ts +7 -0
- package/dist/visualize/template-graph-3d.d.ts.map +1 -0
- package/dist/visualize/template-graph-3d.js +1114 -0
- package/dist/visualize/template-graph-3d.js.map +1 -0
- package/dist/visualize/template-graph-vis.d.ts +8 -0
- package/dist/visualize/template-graph-vis.d.ts.map +1 -0
- package/dist/visualize/template-graph-vis.js +1215 -0
- package/dist/visualize/template-graph-vis.js.map +1 -0
- package/dist/visualize/template-html.d.ts +9 -0
- package/dist/visualize/template-html.d.ts.map +1 -0
- package/dist/visualize/template-html.js +635 -0
- package/dist/visualize/template-html.js.map +1 -0
- package/dist/visualize/template-md-viewer.d.ts +11 -0
- package/dist/visualize/template-md-viewer.d.ts.map +1 -0
- package/dist/visualize/template-md-viewer.js +806 -0
- package/dist/visualize/template-md-viewer.js.map +1 -0
- package/dist/visualize/template-pages.d.ts +7 -0
- package/dist/visualize/template-pages.d.ts.map +1 -0
- package/dist/visualize/template-pages.js +1892 -0
- package/dist/visualize/template-pages.js.map +1 -0
- package/dist/visualize/template-stats-modal.d.ts +7 -0
- package/dist/visualize/template-stats-modal.d.ts.map +1 -0
- package/dist/visualize/template-stats-modal.js +466 -0
- package/dist/visualize/template-stats-modal.js.map +1 -0
- package/dist/visualize/template-styles.d.ts +9 -0
- package/dist/visualize/template-styles.d.ts.map +1 -0
- package/dist/visualize/template-styles.js +623 -0
- package/dist/visualize/template-styles.js.map +1 -0
- package/dist/visualize/template.d.ts +15 -3
- package/dist/visualize/template.d.ts.map +1 -1
- package/dist/visualize/template.js +44 -3475
- package/dist/visualize/template.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1892 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DevPlan 图可视化 — 页面模块
|
|
4
|
+
*
|
|
5
|
+
* 包含: 文档浏览页、RAG 聊天、统计仪表盘。
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.getPagesScript = getPagesScript;
|
|
9
|
+
function getPagesScript() {
|
|
10
|
+
return `
|
|
11
|
+
// ========== Docs Browser ==========
|
|
12
|
+
var docsLoaded = false;
|
|
13
|
+
var docsData = []; // 全部文档列表
|
|
14
|
+
var currentDocKey = ''; // 当前选中文档的 key (section|subSection)
|
|
15
|
+
|
|
16
|
+
/** 根据 docKey 从 docsData 中查找文档标题 */
|
|
17
|
+
function findDocTitle(docKey) {
|
|
18
|
+
for (var i = 0; i < docsData.length; i++) {
|
|
19
|
+
var d = docsData[i];
|
|
20
|
+
var key = d.section + (d.subSection ? '|' + d.subSection : '');
|
|
21
|
+
if (key === docKey) return d.title;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Section 类型的中文名称映射 */
|
|
27
|
+
var SECTION_NAMES = {
|
|
28
|
+
overview: '概述', core_concepts: '核心概念', api_design: 'API 设计',
|
|
29
|
+
file_structure: '文件结构', config: '配置', examples: '使用示例',
|
|
30
|
+
technical_notes: '技术笔记', api_endpoints: 'API 端点',
|
|
31
|
+
milestones: '里程碑', changelog: '变更记录', custom: '自定义'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Section 图标映射(使用简洁符号替代 emoji) */
|
|
35
|
+
var SECTION_ICONS = {
|
|
36
|
+
overview: '▸', core_concepts: '▸', api_design: '▸',
|
|
37
|
+
file_structure: '▸', config: '▸', examples: '▸',
|
|
38
|
+
technical_notes: '▸', api_endpoints: '▸',
|
|
39
|
+
milestones: '▸', changelog: '▸', custom: '▸'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** 加载文档数据(全局共享,仅请求一次) */
|
|
43
|
+
function loadDocsData(callback) {
|
|
44
|
+
if (docsLoaded && docsData.length > 0) {
|
|
45
|
+
if (callback) callback(docsData, null);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
fetch('/api/docs').then(function(r) { return r.json(); }).then(function(data) {
|
|
49
|
+
docsData = data.docs || [];
|
|
50
|
+
docsLoaded = true;
|
|
51
|
+
if (callback) callback(docsData, null);
|
|
52
|
+
}).catch(function(err) {
|
|
53
|
+
if (callback) callback(null, err);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadDocsPage() {
|
|
58
|
+
var list = document.getElementById('docsGroupList');
|
|
59
|
+
// 数据已加载(可能由全局文档弹层预先加载),直接渲染到侧边栏
|
|
60
|
+
if (docsLoaded && docsData.length > 0) {
|
|
61
|
+
renderDocsList(docsData);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (list) list.innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;font-size:12px;"><div class="spinner" style="margin:0 auto 12px;width:24px;height:24px;border-width:3px;"></div>加载文档列表...</div>';
|
|
65
|
+
|
|
66
|
+
loadDocsData(function(data, err) {
|
|
67
|
+
if (err) {
|
|
68
|
+
if (list) list.innerHTML = '<div style="text-align:center;padding:40px;color:#f87171;font-size:12px;">加载失败: ' + err.message + '<br><span style="cursor:pointer;color:#818cf8;text-decoration:underline;" onclick="docsLoaded=false;loadDocsPage();">重试</span></div>';
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
renderDocsList(docsData);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 获取文档的 key(唯一标识) */
|
|
76
|
+
function docItemKey(item) {
|
|
77
|
+
return item.section + (item.subSection ? '|' + item.subSection : '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 记录哪些父文档处于折叠状态(key → true 表示折叠) */
|
|
81
|
+
var docsCollapsedState = {};
|
|
82
|
+
|
|
83
|
+
/** 将文档列表按 section 分组渲染,支持 parentDoc 层级
|
|
84
|
+
* @param docs - 文档数组
|
|
85
|
+
* @param targetId - 渲染目标容器 ID (默认 'docsGroupList')
|
|
86
|
+
* @param selectFn - 点击文档时调用的函数名 (默认 'selectDoc')
|
|
87
|
+
*/
|
|
88
|
+
function renderDocsList(docs, targetId, selectFn) {
|
|
89
|
+
var list = document.getElementById(targetId || 'docsGroupList');
|
|
90
|
+
if (!list) return;
|
|
91
|
+
selectFn = selectFn || 'selectDoc';
|
|
92
|
+
|
|
93
|
+
// 建立 parentDoc → children 映射,区分顶级和子文档
|
|
94
|
+
var childrenMap = {}; // parentDocKey → [child items]
|
|
95
|
+
var childKeySet = {}; // 属于子文档的 key 集合
|
|
96
|
+
for (var i = 0; i < docs.length; i++) {
|
|
97
|
+
var d = docs[i];
|
|
98
|
+
if (d.parentDoc) {
|
|
99
|
+
if (!childrenMap[d.parentDoc]) childrenMap[d.parentDoc] = [];
|
|
100
|
+
childrenMap[d.parentDoc].push(d);
|
|
101
|
+
childKeySet[docItemKey(d)] = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 按 section 分组(只放顶级文档)
|
|
106
|
+
var groups = {};
|
|
107
|
+
var groupOrder = [];
|
|
108
|
+
for (var i = 0; i < docs.length; i++) {
|
|
109
|
+
var d = docs[i];
|
|
110
|
+
var key = docItemKey(d);
|
|
111
|
+
if (childKeySet[key]) continue; // 跳过子文档(由父文档渲染)
|
|
112
|
+
var sec = d.section;
|
|
113
|
+
if (!groups[sec]) {
|
|
114
|
+
groups[sec] = [];
|
|
115
|
+
groupOrder.push(sec);
|
|
116
|
+
}
|
|
117
|
+
groups[sec].push(d);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 每组内按 updatedAt 倒序排列(最新的在上方)
|
|
121
|
+
for (var gi = 0; gi < groupOrder.length; gi++) {
|
|
122
|
+
groups[groupOrder[gi]].sort(function(a, b) {
|
|
123
|
+
var ta = a.updatedAt || 0;
|
|
124
|
+
var tb = b.updatedAt || 0;
|
|
125
|
+
return tb - ta; // 降序
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 子文档也按 updatedAt 倒序
|
|
130
|
+
var parentKeys = Object.keys(childrenMap);
|
|
131
|
+
for (var pi = 0; pi < parentKeys.length; pi++) {
|
|
132
|
+
childrenMap[parentKeys[pi]].sort(function(a, b) {
|
|
133
|
+
var ta = a.updatedAt || 0;
|
|
134
|
+
var tb = b.updatedAt || 0;
|
|
135
|
+
return tb - ta;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 分组按最新文档日期排序(最新的分组在上)
|
|
140
|
+
groupOrder.sort(function(secA, secB) {
|
|
141
|
+
var maxA = 0, maxB = 0;
|
|
142
|
+
var itemsA = groups[secA] || [];
|
|
143
|
+
var itemsB = groups[secB] || [];
|
|
144
|
+
for (var k = 0; k < itemsA.length; k++) {
|
|
145
|
+
if ((itemsA[k].updatedAt || 0) > maxA) maxA = itemsA[k].updatedAt || 0;
|
|
146
|
+
}
|
|
147
|
+
for (var k = 0; k < itemsB.length; k++) {
|
|
148
|
+
if ((itemsB[k].updatedAt || 0) > maxB) maxB = itemsB[k].updatedAt || 0;
|
|
149
|
+
}
|
|
150
|
+
return maxB - maxA;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (groupOrder.length === 0) {
|
|
154
|
+
list.innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;font-size:12px;">暂无文档</div>';
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
var html = '';
|
|
159
|
+
for (var gi = 0; gi < groupOrder.length; gi++) {
|
|
160
|
+
var sec = groupOrder[gi];
|
|
161
|
+
var items = groups[sec];
|
|
162
|
+
var secName = SECTION_NAMES[sec] || sec;
|
|
163
|
+
var secIcon = SECTION_ICONS[sec] || '▸';
|
|
164
|
+
|
|
165
|
+
// 计算此分组下文档总数(含子文档)
|
|
166
|
+
var totalCount = 0;
|
|
167
|
+
for (var ci = 0; ci < docs.length; ci++) {
|
|
168
|
+
if (docs[ci].section === sec) totalCount++;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
html += '<div class="docs-group" data-section="' + sec + '">';
|
|
172
|
+
html += '<div class="docs-group-title" onclick="toggleDocsGroup(this)">';
|
|
173
|
+
html += '<span class="docs-group-arrow">▼</span>';
|
|
174
|
+
html += '<span>' + secName + '</span>';
|
|
175
|
+
html += '<span class="docs-group-count">' + totalCount + '</span>';
|
|
176
|
+
html += '</div>';
|
|
177
|
+
html += '<div class="docs-group-items">';
|
|
178
|
+
|
|
179
|
+
for (var ii = 0; ii < items.length; ii++) {
|
|
180
|
+
html += renderDocItemWithChildren(items[ii], childrenMap, secIcon, selectFn);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
html += '</div></div>';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
list.innerHTML = html;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 递归渲染文档项及其子文档
|
|
190
|
+
* @param selectFn - 点击时调用的函数名 (默认 'selectDoc')
|
|
191
|
+
*/
|
|
192
|
+
function renderDocItemWithChildren(item, childrenMap, secIcon, selectFn) {
|
|
193
|
+
selectFn = selectFn || 'selectDoc';
|
|
194
|
+
var docKey = docItemKey(item);
|
|
195
|
+
var isActive = docKey === currentDocKey ? ' active' : '';
|
|
196
|
+
var children = childrenMap[docKey] || [];
|
|
197
|
+
var hasChildren = children.length > 0;
|
|
198
|
+
var isCollapsed = docsCollapsedState[docKey] === true;
|
|
199
|
+
|
|
200
|
+
var html = '<div class="docs-item-wrapper">';
|
|
201
|
+
|
|
202
|
+
// 文档项本身
|
|
203
|
+
html += '<div class="docs-item' + isActive + '" data-key="' + escHtml(docKey) + '" onclick="' + selectFn + '(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
204
|
+
|
|
205
|
+
if (hasChildren) {
|
|
206
|
+
var toggleIcon = isCollapsed ? '+' : '−';
|
|
207
|
+
html += '<span class="docs-item-toggle" onclick="event.stopPropagation();toggleDocChildren(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)" title="' + (isCollapsed ? '展开子文档' : '收起子文档') + '">' + toggleIcon + '</span>';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 不显示 emoji 图标,仅保留标题
|
|
211
|
+
html += '<span class="docs-item-text" title="' + escHtml(item.title) + '">' + escHtml(item.title) + '</span>';
|
|
212
|
+
if (hasChildren) {
|
|
213
|
+
html += '<span class="docs-item-sub" style="color:#818cf8;">' + children.length + ' 子文档</span>';
|
|
214
|
+
}
|
|
215
|
+
// 右侧显示日期(替代原来的 subSection 标签)
|
|
216
|
+
if (item.updatedAt) {
|
|
217
|
+
html += '<span class="docs-item-sub">' + fmtDateShort(item.updatedAt) + '</span>';
|
|
218
|
+
}
|
|
219
|
+
html += '</div>';
|
|
220
|
+
|
|
221
|
+
// 子文档列表
|
|
222
|
+
if (hasChildren) {
|
|
223
|
+
html += '<div class="docs-children' + (isCollapsed ? ' collapsed' : '') + '" data-parent="' + escHtml(docKey) + '">';
|
|
224
|
+
for (var ci = 0; ci < children.length; ci++) {
|
|
225
|
+
html += renderDocItemWithChildren(children[ci], childrenMap, secIcon, selectFn);
|
|
226
|
+
}
|
|
227
|
+
html += '</div>';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
html += '</div>';
|
|
231
|
+
return html;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 展开/折叠子文档 */
|
|
235
|
+
function toggleDocChildren(docKey) {
|
|
236
|
+
docsCollapsedState[docKey] = !docsCollapsedState[docKey];
|
|
237
|
+
var container = document.querySelector('.docs-children[data-parent="' + docKey + '"]');
|
|
238
|
+
if (!container) return;
|
|
239
|
+
container.classList.toggle('collapsed');
|
|
240
|
+
// 更新切换按钮图标
|
|
241
|
+
var wrapper = container.previousElementSibling;
|
|
242
|
+
if (wrapper) {
|
|
243
|
+
var toggle = wrapper.querySelector('.docs-item-toggle');
|
|
244
|
+
if (toggle) {
|
|
245
|
+
toggle.textContent = docsCollapsedState[docKey] ? '+' : '−';
|
|
246
|
+
toggle.title = docsCollapsedState[docKey] ? '展开子文档' : '收起子文档';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** 展开/折叠文档分组 */
|
|
252
|
+
function toggleDocsGroup(el) {
|
|
253
|
+
var group = el.closest('.docs-group');
|
|
254
|
+
if (group) group.classList.toggle('collapsed');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 控制搜索框清除按钮的显示/隐藏
|
|
258
|
+
* @param searchId - 搜索输入框 ID (默认 'docsSearch')
|
|
259
|
+
* @param clearBtnId - 清除按钮 ID (默认 'docsSearchClear')
|
|
260
|
+
*/
|
|
261
|
+
function toggleSearchClear(searchId, clearBtnId) {
|
|
262
|
+
var input = document.getElementById(searchId || 'docsSearch');
|
|
263
|
+
var btn = document.getElementById(clearBtnId || 'docsSearchClear');
|
|
264
|
+
if (input && btn) {
|
|
265
|
+
if (input.value.length > 0) { btn.classList.add('show'); } else { btn.classList.remove('show'); }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 清空搜索框并重置列表
|
|
270
|
+
* @param searchId - 搜索输入框 ID (默认 'docsSearch')
|
|
271
|
+
* @param clearBtnId - 清除按钮 ID (默认 'docsSearchClear')
|
|
272
|
+
* @param targetId - 渲染目标容器 ID (默认 'docsGroupList')
|
|
273
|
+
* @param selectFn - 点击文档时调用的函数名 (默认 'selectDoc')
|
|
274
|
+
*/
|
|
275
|
+
function clearDocsSearch(searchId, clearBtnId, targetId, selectFn) {
|
|
276
|
+
var input = document.getElementById(searchId || 'docsSearch');
|
|
277
|
+
if (input) { input.value = ''; input.focus(); }
|
|
278
|
+
toggleSearchClear(searchId, clearBtnId);
|
|
279
|
+
filterDocs(searchId, targetId, selectFn);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 搜索过滤文档列表
|
|
283
|
+
* @param searchId - 搜索输入框 ID (默认 'docsSearch')
|
|
284
|
+
* @param targetId - 渲染目标容器 ID (默认 'docsGroupList')
|
|
285
|
+
* @param selectFn - 点击文档时调用的函数名 (默认 'selectDoc')
|
|
286
|
+
*/
|
|
287
|
+
function filterDocs(searchId, targetId, selectFn) {
|
|
288
|
+
var input = document.getElementById(searchId || 'docsSearch');
|
|
289
|
+
var query = (input ? input.value || '' : '').toLowerCase().trim();
|
|
290
|
+
if (!query) {
|
|
291
|
+
renderDocsList(docsData, targetId, selectFn);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
var filtered = [];
|
|
295
|
+
for (var i = 0; i < docsData.length; i++) {
|
|
296
|
+
var d = docsData[i];
|
|
297
|
+
var text = (d.title || '') + ' ' + (d.section || '') + ' ' + (d.subSection || '');
|
|
298
|
+
if (text.toLowerCase().indexOf(query) >= 0) {
|
|
299
|
+
filtered.push(d);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
renderDocsList(filtered, targetId, selectFn);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 选中并加载文档内容 */
|
|
306
|
+
function selectDoc(docKey) {
|
|
307
|
+
currentDocKey = docKey;
|
|
308
|
+
|
|
309
|
+
// 更新左侧选中状态
|
|
310
|
+
var items = document.querySelectorAll('.docs-item');
|
|
311
|
+
for (var i = 0; i < items.length; i++) {
|
|
312
|
+
items[i].classList.remove('active');
|
|
313
|
+
if (items[i].getAttribute('data-key') === docKey) {
|
|
314
|
+
items[i].classList.add('active');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 解析 key
|
|
319
|
+
var parts = docKey.split('|');
|
|
320
|
+
var section = parts[0];
|
|
321
|
+
var subSection = parts[1] || null;
|
|
322
|
+
|
|
323
|
+
// 显示内容区,隐藏空状态
|
|
324
|
+
document.getElementById('docsEmptyState').style.display = 'none';
|
|
325
|
+
var contentView = document.getElementById('docsContentView');
|
|
326
|
+
contentView.style.display = 'flex';
|
|
327
|
+
|
|
328
|
+
// 显示加载状态
|
|
329
|
+
document.getElementById('docsContentTitle').textContent = '加载中...';
|
|
330
|
+
document.getElementById('docsContentMeta').innerHTML = '';
|
|
331
|
+
document.getElementById('docsContentInner').innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;"><div class="spinner" style="margin:0 auto 12px;width:24px;height:24px;border-width:3px;"></div></div>';
|
|
332
|
+
|
|
333
|
+
// 请求文档内容
|
|
334
|
+
var url = '/api/doc?section=' + encodeURIComponent(section);
|
|
335
|
+
if (subSection) url += '&subSection=' + encodeURIComponent(subSection);
|
|
336
|
+
|
|
337
|
+
fetch(url).then(function(r) {
|
|
338
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
339
|
+
return r.json();
|
|
340
|
+
}).then(function(doc) {
|
|
341
|
+
renderDocContent(doc, section, subSection);
|
|
342
|
+
}).catch(function(err) {
|
|
343
|
+
document.getElementById('docsContentTitle').textContent = '加载失败';
|
|
344
|
+
document.getElementById('docsContentInner').innerHTML = '<div style="text-align:center;padding:40px;color:#f87171;">加载失败: ' + escHtml(err.message) + '</div>';
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 渲染文档内容到右侧面板 */
|
|
349
|
+
function renderDocContent(doc, section, subSection) {
|
|
350
|
+
var secName = SECTION_NAMES[section] || section;
|
|
351
|
+
|
|
352
|
+
// 标题
|
|
353
|
+
document.getElementById('docsContentTitle').textContent = doc.title || secName;
|
|
354
|
+
|
|
355
|
+
// 元信息标签
|
|
356
|
+
var metaHtml = '<span class="docs-content-tag section">' + secName + '</span>';
|
|
357
|
+
if (subSection) {
|
|
358
|
+
metaHtml += '<span class="docs-content-tag section">' + escHtml(subSection) + '</span>';
|
|
359
|
+
}
|
|
360
|
+
if (doc.version) {
|
|
361
|
+
metaHtml += '<span class="docs-content-tag version">v' + escHtml(doc.version) + '</span>';
|
|
362
|
+
}
|
|
363
|
+
if (doc.updatedAt) {
|
|
364
|
+
metaHtml += '<span class="docs-content-tag">' + fmtTime(doc.updatedAt) + '</span>';
|
|
365
|
+
}
|
|
366
|
+
if (doc.id) {
|
|
367
|
+
metaHtml += '<span class="docs-content-tag docs-id-tag" title="点击复制 ID" style="cursor:pointer;font-family:monospace;font-size:10px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" onclick="navigator.clipboard.writeText(\\x27' + escHtml(doc.id) + '\\x27).then(function(){var t=event.target;t.textContent=\\x27✓ 已复制\\x27;setTimeout(function(){t.textContent=\\x27ID: ' + escHtml(doc.id.slice(0,8)) + '…\\x27},1200)})">ID: ' + escHtml(doc.id.slice(0,8)) + '…</span>';
|
|
368
|
+
}
|
|
369
|
+
document.getElementById('docsContentMeta').innerHTML = metaHtml;
|
|
370
|
+
|
|
371
|
+
// Markdown 内容
|
|
372
|
+
var contentHtml = '';
|
|
373
|
+
if (doc.content) {
|
|
374
|
+
contentHtml = renderMarkdown(doc.content);
|
|
375
|
+
} else {
|
|
376
|
+
contentHtml = '<div style="text-align:center;padding:40px;color:#6b7280;">文档内容为空</div>';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 父文档链接
|
|
380
|
+
if (doc.parentDoc) {
|
|
381
|
+
var parentTitle = findDocTitle(doc.parentDoc);
|
|
382
|
+
contentHtml += '<div class="docs-related" style="margin-top: 12px;">';
|
|
383
|
+
contentHtml += '<div class="docs-related-title">⬆️ 父文档</div>';
|
|
384
|
+
contentHtml += '<div class="docs-related-item" style="cursor:pointer;" onclick="selectDoc(\\x27' + doc.parentDoc.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
385
|
+
contentHtml += '<span class="rel-icon" style="background:#1e3a5f;color:#93c5fd;">📄</span>';
|
|
386
|
+
contentHtml += '<span style="flex:1;color:#818cf8;">' + escHtml(parentTitle || doc.parentDoc) + '</span>';
|
|
387
|
+
contentHtml += '<span style="font-size:10px;color:#6b7280;font-family:monospace;">' + escHtml(doc.parentDoc) + '</span>';
|
|
388
|
+
contentHtml += '</div></div>';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 子文档列表
|
|
392
|
+
var childDocs = doc.childDocs || [];
|
|
393
|
+
if (childDocs.length > 0) {
|
|
394
|
+
contentHtml += '<div class="docs-related" style="margin-top: 12px;">';
|
|
395
|
+
contentHtml += '<div class="docs-related-title">⬇️ 子文档 (' + childDocs.length + ')</div>';
|
|
396
|
+
for (var ci = 0; ci < childDocs.length; ci++) {
|
|
397
|
+
var childKey = childDocs[ci];
|
|
398
|
+
var childTitle = findDocTitle(childKey);
|
|
399
|
+
contentHtml += '<div class="docs-related-item" style="cursor:pointer;" onclick="selectDoc(\\x27' + childKey.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
400
|
+
contentHtml += '<span class="rel-icon" style="background:#1e1b4b;color:#c084fc;">📄</span>';
|
|
401
|
+
contentHtml += '<span style="flex:1;color:#c084fc;">' + escHtml(childTitle || childKey) + '</span>';
|
|
402
|
+
contentHtml += '<span style="font-size:10px;color:#6b7280;font-family:monospace;">' + escHtml(childKey) + '</span>';
|
|
403
|
+
contentHtml += '</div>';
|
|
404
|
+
}
|
|
405
|
+
contentHtml += '</div>';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 关联任务
|
|
409
|
+
var relatedTasks = doc.relatedTasks || [];
|
|
410
|
+
if (relatedTasks.length > 0) {
|
|
411
|
+
contentHtml += '<div class="docs-related">';
|
|
412
|
+
contentHtml += '<div class="docs-related-title">🔗 关联任务 (' + relatedTasks.length + ')</div>';
|
|
413
|
+
for (var i = 0; i < relatedTasks.length; i++) {
|
|
414
|
+
var t = relatedTasks[i];
|
|
415
|
+
var tStatus = t.status || 'pending';
|
|
416
|
+
var tIcon = tStatus === 'completed' ? '✓' : tStatus === 'in_progress' ? '▶' : '○';
|
|
417
|
+
var iconBg = tStatus === 'completed' ? '#064e3b' : tStatus === 'in_progress' ? '#1e3a5f' : '#374151';
|
|
418
|
+
var iconColor = tStatus === 'completed' ? '#6ee7b7' : tStatus === 'in_progress' ? '#93c5fd' : '#6b7280';
|
|
419
|
+
contentHtml += '<div class="docs-related-item">';
|
|
420
|
+
contentHtml += '<span class="rel-icon" style="background:' + iconBg + ';color:' + iconColor + ';">' + tIcon + '</span>';
|
|
421
|
+
contentHtml += '<span style="flex:1;">' + escHtml(t.title) + '</span>';
|
|
422
|
+
contentHtml += '<span style="font-size:10px;color:#6b7280;font-family:monospace;">' + escHtml(t.taskId) + '</span>';
|
|
423
|
+
if (t.priority) {
|
|
424
|
+
contentHtml += '<span class="status-badge priority-' + t.priority + '" style="font-size:10px;">' + t.priority + '</span>';
|
|
425
|
+
}
|
|
426
|
+
contentHtml += '</div>';
|
|
427
|
+
}
|
|
428
|
+
contentHtml += '</div>';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
var innerEl = document.getElementById('docsContentInner');
|
|
432
|
+
innerEl.innerHTML = contentHtml;
|
|
433
|
+
// 后处理:代码高亮、复制按钮、表格包裹等
|
|
434
|
+
if (typeof mdEnhanceContent === 'function') mdEnhanceContent(innerEl);
|
|
435
|
+
// 生成右侧目录导航
|
|
436
|
+
docsBuildToc();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ========== Docs TOC ==========
|
|
440
|
+
var _docsTocCleanup = null;
|
|
441
|
+
|
|
442
|
+
function docsBuildToc() {
|
|
443
|
+
var tocList = document.getElementById('docsTocList');
|
|
444
|
+
var tocPanel = document.getElementById('docsTocPanel');
|
|
445
|
+
var inner = document.getElementById('docsContentInner');
|
|
446
|
+
if (!tocList || !tocPanel || !inner) return;
|
|
447
|
+
tocList.innerHTML = '';
|
|
448
|
+
|
|
449
|
+
var headings = inner.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
450
|
+
if (headings.length < 2) {
|
|
451
|
+
tocPanel.style.display = 'none';
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
var minLv = 6;
|
|
456
|
+
for (var i = 0; i < headings.length; i++) {
|
|
457
|
+
var lv = parseInt(headings[i].tagName[1]);
|
|
458
|
+
if (lv < minLv) minLv = lv;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (var i = 0; i < headings.length; i++) {
|
|
462
|
+
var h = headings[i];
|
|
463
|
+
var indent = parseInt(h.tagName[1]) - minLv;
|
|
464
|
+
var li = document.createElement('li');
|
|
465
|
+
var a = document.createElement('a');
|
|
466
|
+
a.href = '#' + h.id;
|
|
467
|
+
a.textContent = h.textContent;
|
|
468
|
+
a.dataset.tid = h.id;
|
|
469
|
+
if (indent > 0) a.className = 'indent-' + Math.min(indent, 4);
|
|
470
|
+
a.onclick = (function(target) {
|
|
471
|
+
return function(e) {
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
474
|
+
};
|
|
475
|
+
})(h);
|
|
476
|
+
li.appendChild(a);
|
|
477
|
+
tocList.appendChild(li);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
tocPanel.style.display = '';
|
|
481
|
+
docsSetupScrollSpy(headings);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function docsSetupScrollSpy(headings) {
|
|
485
|
+
var scrollArea = document.getElementById('docsContentBody');
|
|
486
|
+
var tocList = document.getElementById('docsTocList');
|
|
487
|
+
if (!scrollArea || !tocList) return;
|
|
488
|
+
|
|
489
|
+
// 清理之前的监听器
|
|
490
|
+
if (_docsTocCleanup) { _docsTocCleanup(); _docsTocCleanup = null; }
|
|
491
|
+
|
|
492
|
+
var links = tocList.querySelectorAll('a');
|
|
493
|
+
var ticking = false;
|
|
494
|
+
|
|
495
|
+
function onScroll() {
|
|
496
|
+
if (ticking) return;
|
|
497
|
+
ticking = true;
|
|
498
|
+
requestAnimationFrame(function() {
|
|
499
|
+
var cur = '';
|
|
500
|
+
for (var i = 0; i < headings.length; i++) {
|
|
501
|
+
var rect = headings[i].getBoundingClientRect();
|
|
502
|
+
if (rect.top <= 120) cur = headings[i].id;
|
|
503
|
+
}
|
|
504
|
+
for (var i = 0; i < links.length; i++) {
|
|
505
|
+
links[i].classList.toggle('active', links[i].dataset.tid === cur);
|
|
506
|
+
}
|
|
507
|
+
ticking = false;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
scrollArea.addEventListener('scroll', onScroll, { passive: true });
|
|
512
|
+
_docsTocCleanup = function() {
|
|
513
|
+
scrollArea.removeEventListener('scroll', onScroll);
|
|
514
|
+
};
|
|
515
|
+
onScroll();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ========== RAG Chat ==========
|
|
519
|
+
var chatHistory = []; // [{role:'user'|'assistant', content:string, results?:array}]
|
|
520
|
+
var chatBusy = false;
|
|
521
|
+
|
|
522
|
+
/** 点击推荐话题 */
|
|
523
|
+
function chatSendTip(el) {
|
|
524
|
+
var input = document.getElementById('docsChatInput');
|
|
525
|
+
if (input) { input.value = el.textContent; chatSend(); }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Enter 发送(Shift+Enter 换行) */
|
|
529
|
+
function chatInputKeydown(e) {
|
|
530
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
531
|
+
e.preventDefault();
|
|
532
|
+
chatSend();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** 自动调整 textarea 高度 */
|
|
537
|
+
function chatAutoResize(el) {
|
|
538
|
+
el.style.height = 'auto';
|
|
539
|
+
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** 发送消息并搜索 */
|
|
543
|
+
function chatSend() {
|
|
544
|
+
if (chatBusy) return;
|
|
545
|
+
var input = document.getElementById('docsChatInput');
|
|
546
|
+
var query = (input.value || '').trim();
|
|
547
|
+
if (!query) return;
|
|
548
|
+
|
|
549
|
+
// 隐藏欢迎信息
|
|
550
|
+
var welcome = document.getElementById('docsChatWelcome');
|
|
551
|
+
if (welcome) welcome.style.display = 'none';
|
|
552
|
+
|
|
553
|
+
// 添加用户消息
|
|
554
|
+
chatHistory.push({ role: 'user', content: query });
|
|
555
|
+
chatRenderBubble('user', query);
|
|
556
|
+
input.value = '';
|
|
557
|
+
chatAutoResize(input);
|
|
558
|
+
|
|
559
|
+
// 显示加载动画
|
|
560
|
+
chatBusy = true;
|
|
561
|
+
document.getElementById('docsChatSend').disabled = true;
|
|
562
|
+
var loadingId = 'chat-loading-' + Date.now();
|
|
563
|
+
var msgBox = document.getElementById('docsChatMessages');
|
|
564
|
+
var loadingHtml = '<div class="chat-bubble assistant" id="' + loadingId + '"><div class="chat-bubble-inner"><div class="chat-typing"><div class="chat-typing-dot"></div><div class="chat-typing-dot"></div><div class="chat-typing-dot"></div></div></div></div>';
|
|
565
|
+
msgBox.insertAdjacentHTML('beforeend', loadingHtml);
|
|
566
|
+
msgBox.scrollTop = msgBox.scrollHeight;
|
|
567
|
+
|
|
568
|
+
// 调用搜索 API
|
|
569
|
+
fetch('/api/chat', {
|
|
570
|
+
method: 'POST',
|
|
571
|
+
headers: { 'Content-Type': 'application/json' },
|
|
572
|
+
body: JSON.stringify({ query: query, limit: 5 })
|
|
573
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
574
|
+
// 移除加载动画
|
|
575
|
+
var loadEl = document.getElementById(loadingId);
|
|
576
|
+
if (loadEl) loadEl.remove();
|
|
577
|
+
|
|
578
|
+
var replyHtml = '';
|
|
579
|
+
|
|
580
|
+
if (data.type === 'meta') {
|
|
581
|
+
// ---- 元信息直接回答 ----
|
|
582
|
+
replyHtml = chatFormatMarkdown(data.answer || '');
|
|
583
|
+
} else {
|
|
584
|
+
// ---- 文档搜索结果 ----
|
|
585
|
+
var results = data.results || [];
|
|
586
|
+
if (results.length > 0) {
|
|
587
|
+
replyHtml += '<div style="margin-bottom:8px;color:#9ca3af;font-size:12px;">找到 <strong style="color:#a5b4fc;">' + results.length + '</strong> 篇相关文档';
|
|
588
|
+
if (data.mode === 'hybrid') replyHtml += ' <span style="font-size:10px;color:#6b7280;">(语义+字面混合)</span>';
|
|
589
|
+
else if (data.mode === 'semantic') replyHtml += ' <span style="font-size:10px;color:#6b7280;">(语义搜索)</span>';
|
|
590
|
+
else replyHtml += ' <span style="font-size:10px;color:#6b7280;">(字面搜索)</span>';
|
|
591
|
+
replyHtml += '</div>';
|
|
592
|
+
|
|
593
|
+
for (var i = 0; i < results.length; i++) {
|
|
594
|
+
var r = results[i];
|
|
595
|
+
var docKey = r.section + (r.subSection ? '|' + r.subSection : '');
|
|
596
|
+
replyHtml += '<div class="chat-result-card" onclick="chatOpenDoc(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)">';
|
|
597
|
+
replyHtml += '<div class="chat-result-title">';
|
|
598
|
+
replyHtml += '<span>📄 ' + escHtml(r.title) + '</span>';
|
|
599
|
+
if (r.score != null) replyHtml += '<span class="chat-result-score">' + r.score.toFixed(3) + '</span>';
|
|
600
|
+
replyHtml += '</div>';
|
|
601
|
+
if (r.snippet) replyHtml += '<div class="chat-result-snippet">' + escHtml(r.snippet) + '</div>';
|
|
602
|
+
var metaParts = [];
|
|
603
|
+
if (r.section) metaParts.push(r.section);
|
|
604
|
+
if (r.updatedAt) metaParts.push(fmtDateShort(r.updatedAt));
|
|
605
|
+
if (r.version) metaParts.push('v' + r.version);
|
|
606
|
+
if (metaParts.length > 0) replyHtml += '<div class="chat-result-meta">' + metaParts.join(' · ') + '</div>';
|
|
607
|
+
replyHtml += '</div>';
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
replyHtml += '<div class="chat-no-result">🤔 未找到高度相关的文档。</div>';
|
|
611
|
+
replyHtml += '<div style="margin-top:8px;font-size:12px;color:#6b7280;line-height:1.6;">';
|
|
612
|
+
replyHtml += '建议:<br>';
|
|
613
|
+
replyHtml += '• 尝试使用更具体的 <strong>关键词</strong>(如 "向量搜索"、"GPU"、"LanceDB")<br>';
|
|
614
|
+
replyHtml += '• 问项目统计问题(如 "有多少篇文档"、"项目进度"、"有哪些阶段")<br>';
|
|
615
|
+
replyHtml += '• 输入 <strong>"帮助"</strong> 查看我的全部能力';
|
|
616
|
+
replyHtml += '</div>';
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
chatHistory.push({ role: 'assistant', content: replyHtml, results: data.results || [] });
|
|
621
|
+
chatRenderBubble('assistant', replyHtml, true);
|
|
622
|
+
|
|
623
|
+
}).catch(function(err) {
|
|
624
|
+
var loadEl = document.getElementById(loadingId);
|
|
625
|
+
if (loadEl) loadEl.remove();
|
|
626
|
+
chatRenderBubble('assistant', '<span style="color:#f87171;">搜索出错: ' + escHtml(err.message) + '</span>', true);
|
|
627
|
+
}).finally(function() {
|
|
628
|
+
chatBusy = false;
|
|
629
|
+
document.getElementById('docsChatSend').disabled = false;
|
|
630
|
+
document.getElementById('docsChatInput').focus();
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** 简单 Markdown → HTML 转换(用于元信息回答) */
|
|
635
|
+
function chatFormatMarkdown(text) {
|
|
636
|
+
return text
|
|
637
|
+
.replace(/\\*\\*(.+?)\\*\\*/g, '<strong style="color:#a5b4fc;">$1</strong>')
|
|
638
|
+
.replace(/\\n/g, '<br>');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** 渲染一条消息气泡 */
|
|
642
|
+
function chatRenderBubble(role, content, isHtml) {
|
|
643
|
+
var msgBox = document.getElementById('docsChatMessages');
|
|
644
|
+
var bubble = document.createElement('div');
|
|
645
|
+
bubble.className = 'chat-bubble ' + role;
|
|
646
|
+
var inner = document.createElement('div');
|
|
647
|
+
inner.className = 'chat-bubble-inner';
|
|
648
|
+
if (isHtml) { inner.innerHTML = content; }
|
|
649
|
+
else { inner.textContent = content; }
|
|
650
|
+
bubble.appendChild(inner);
|
|
651
|
+
msgBox.appendChild(bubble);
|
|
652
|
+
msgBox.scrollTop = msgBox.scrollHeight;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** 从聊天结果中点击打开文档 */
|
|
656
|
+
function chatOpenDoc(docKey) {
|
|
657
|
+
selectDoc(docKey);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** 返回聊天视图 */
|
|
661
|
+
function backToChat() {
|
|
662
|
+
document.getElementById('docsContentView').style.display = 'none';
|
|
663
|
+
document.getElementById('docsEmptyState').style.display = 'flex';
|
|
664
|
+
// 取消左侧选中
|
|
665
|
+
currentDocKey = '';
|
|
666
|
+
var items = document.querySelectorAll('.docs-item');
|
|
667
|
+
for (var i = 0; i < items.length; i++) items[i].classList.remove('active');
|
|
668
|
+
// 聚焦输入框
|
|
669
|
+
var input = document.getElementById('docsChatInput');
|
|
670
|
+
if (input) input.focus();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
// ========== Stats Dashboard ==========
|
|
675
|
+
var statsLoaded = false;
|
|
676
|
+
|
|
677
|
+
function loadStatsPage() {
|
|
678
|
+
var container = document.getElementById('statsContent');
|
|
679
|
+
if (!container) return;
|
|
680
|
+
container.innerHTML = '<div style="text-align:center;padding:60px;color:#6b7280;"><div class="spinner" style="margin:0 auto 12px;"></div>加载统计数据...</div>';
|
|
681
|
+
|
|
682
|
+
fetch('/api/stats').then(function(r) { return r.json(); }).then(function(data) {
|
|
683
|
+
statsLoaded = true;
|
|
684
|
+
renderStatsPage(data);
|
|
685
|
+
}).catch(function(err) {
|
|
686
|
+
container.innerHTML = '<div style="text-align:center;padding:60px;color:#f87171;">加载失败: ' + err.message + '<br><button class="refresh-btn" onclick="loadStatsPage()" style="margin-top:12px;">重试</button></div>';
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function renderStatsPage(data) {
|
|
691
|
+
var container = document.getElementById('statsContent');
|
|
692
|
+
if (!container) return;
|
|
693
|
+
|
|
694
|
+
var pct = data.overallPercent || 0;
|
|
695
|
+
var totalSub = data.subTaskCount || 0;
|
|
696
|
+
var doneSub = data.completedSubTasks || 0;
|
|
697
|
+
var totalMain = data.mainTaskCount || 0;
|
|
698
|
+
var doneMain = data.completedMainTasks || 0;
|
|
699
|
+
var docCount = data.docCount || 0;
|
|
700
|
+
var modCount = data.moduleCount || 0;
|
|
701
|
+
|
|
702
|
+
// 激励语
|
|
703
|
+
var motivate = '';
|
|
704
|
+
if (pct >= 100) motivate = '🎉 项目已全部完成!太棒了!';
|
|
705
|
+
else if (pct >= 75) motivate = '🚀 即将大功告成,冲刺阶段!';
|
|
706
|
+
else if (pct >= 50) motivate = '💪 已过半程,保持节奏!';
|
|
707
|
+
else if (pct >= 25) motivate = '🌱 稳步推进中,继续加油!';
|
|
708
|
+
else if (pct > 0) motivate = '🏗️ 万事开头难,已迈出第一步!';
|
|
709
|
+
else motivate = '📋 项目已规划就绪,开始行动吧!';
|
|
710
|
+
|
|
711
|
+
var html = '';
|
|
712
|
+
|
|
713
|
+
// ===== 总体进度环 =====
|
|
714
|
+
var ringR = 54;
|
|
715
|
+
var ringC = 2 * Math.PI * ringR;
|
|
716
|
+
var ringOffset = ringC - (pct / 100) * ringC;
|
|
717
|
+
html += '<div class="progress-ring-wrap">';
|
|
718
|
+
html += '<svg class="ring-svg" width="140" height="140" viewBox="0 0 140 140">';
|
|
719
|
+
html += '<circle cx="70" cy="70" r="' + ringR + '" stroke="#374151" stroke-width="10" fill="none"/>';
|
|
720
|
+
html += '<circle cx="70" cy="70" r="' + ringR + '" stroke="url(#ringGrad)" stroke-width="10" fill="none" stroke-linecap="round" stroke-dasharray="' + ringC + '" stroke-dashoffset="' + ringOffset + '" transform="rotate(-90 70 70)" style="transition:stroke-dashoffset 1s ease;"/>';
|
|
721
|
+
html += '<defs><linearGradient id="ringGrad" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#10b981"/><stop offset="100%" stop-color="#3b82f6"/></linearGradient></defs>';
|
|
722
|
+
html += '<text x="70" y="65" text-anchor="middle" fill="#f3f4f6" font-size="28" font-weight="800">' + pct + '%</text>';
|
|
723
|
+
html += '<text x="70" y="84" text-anchor="middle" fill="#6b7280" font-size="11">完成率</text>';
|
|
724
|
+
html += '</svg>';
|
|
725
|
+
html += '<div class="progress-ring-info">';
|
|
726
|
+
html += '<h3>项目总体进度</h3>';
|
|
727
|
+
html += '<p>子任务完成 <strong style="color:#10b981;">' + doneSub + '</strong> / ' + totalSub + ',主任务完成 <strong style="color:#3b82f6;">' + doneMain + '</strong> / ' + totalMain + '</p>';
|
|
728
|
+
html += '<div class="motivate">' + motivate + '</div>';
|
|
729
|
+
html += '</div></div>';
|
|
730
|
+
|
|
731
|
+
// ===== 概览卡片 =====
|
|
732
|
+
html += '<div class="stats-grid">';
|
|
733
|
+
html += statCard('📋', totalMain, '主任务', doneMain + ' 已完成', 'blue');
|
|
734
|
+
html += statCard('✅', doneSub, '已完成子任务', '共 ' + totalSub + ' 个子任务', 'green');
|
|
735
|
+
html += statCard('📄', docCount, '文档', Object.keys(data.docBySection || {}).length + ' 种类型', 'purple');
|
|
736
|
+
html += statCard('🧩', modCount, '功能模块', '', 'amber');
|
|
737
|
+
var remainSub = totalSub - doneSub;
|
|
738
|
+
html += statCard('⏳', remainSub, '待完成子任务', remainSub > 0 ? '继续努力!' : '全部完成!', 'rose');
|
|
739
|
+
html += '</div>';
|
|
740
|
+
|
|
741
|
+
// ===== 按优先级统计 =====
|
|
742
|
+
var bp = data.byPriority || {};
|
|
743
|
+
html += '<div class="stats-section">';
|
|
744
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🎯</span> 按优先级统计</div>';
|
|
745
|
+
html += '<div class="priority-bars">';
|
|
746
|
+
var priorities = ['P0', 'P1', 'P2'];
|
|
747
|
+
for (var pi = 0; pi < priorities.length; pi++) {
|
|
748
|
+
var pk = priorities[pi];
|
|
749
|
+
var pd = bp[pk] || { total: 0, completed: 0 };
|
|
750
|
+
var ppct = pd.total > 0 ? Math.round(pd.completed / pd.total * 100) : 0;
|
|
751
|
+
html += '<div class="priority-row">';
|
|
752
|
+
html += '<span class="priority-label ' + pk + '">' + pk + '</span>';
|
|
753
|
+
html += '<div class="priority-bar-track"><div class="priority-bar-fill ' + pk + '" style="width:' + ppct + '%"></div></div>';
|
|
754
|
+
html += '<span class="priority-nums">' + pd.completed + '/' + pd.total + ' (' + ppct + '%)</span>';
|
|
755
|
+
html += '</div>';
|
|
756
|
+
}
|
|
757
|
+
html += '</div></div>';
|
|
758
|
+
|
|
759
|
+
// ===== 进行中的任务 =====
|
|
760
|
+
var inProg = data.inProgressPhases || [];
|
|
761
|
+
if (inProg.length > 0) {
|
|
762
|
+
html += '<div class="stats-section">';
|
|
763
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🔄</span> 进行中 (' + inProg.length + ')</div>';
|
|
764
|
+
html += '<div class="phase-list">';
|
|
765
|
+
for (var ii = 0; ii < inProg.length; ii++) {
|
|
766
|
+
html += phaseItem(inProg[ii], 'in_progress', '▶');
|
|
767
|
+
}
|
|
768
|
+
html += '</div></div>';
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ===== 已完成的里程碑 =====
|
|
772
|
+
var done = data.completedPhases || [];
|
|
773
|
+
if (done.length > 0) {
|
|
774
|
+
html += '<div class="stats-section">';
|
|
775
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🏆</span> 已完成里程碑 (' + done.length + ')</div>';
|
|
776
|
+
html += '<div class="phase-list">';
|
|
777
|
+
for (var di = 0; di < done.length; di++) {
|
|
778
|
+
html += phaseItem(done[di], 'completed', '✓');
|
|
779
|
+
}
|
|
780
|
+
html += '</div></div>';
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ===== 待开始的任务 =====
|
|
784
|
+
var pending = data.pendingPhases || [];
|
|
785
|
+
if (pending.length > 0) {
|
|
786
|
+
html += '<div class="stats-section">';
|
|
787
|
+
html += '<div class="stats-section-title"><span class="sec-icon">📌</span> 待开始 (' + pending.length + ')</div>';
|
|
788
|
+
html += '<div class="phase-list">';
|
|
789
|
+
for (var qi = 0; qi < pending.length; qi++) {
|
|
790
|
+
html += phaseItem(pending[qi], 'pending', '○');
|
|
791
|
+
}
|
|
792
|
+
html += '</div></div>';
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ===== 模块概览 =====
|
|
796
|
+
var mods = data.moduleStats || [];
|
|
797
|
+
if (mods.length > 0) {
|
|
798
|
+
html += '<div class="stats-section">';
|
|
799
|
+
html += '<div class="stats-section-title"><span class="sec-icon">🧩</span> 模块概览</div>';
|
|
800
|
+
html += '<div class="module-grid">';
|
|
801
|
+
for (var mi = 0; mi < mods.length; mi++) {
|
|
802
|
+
var mod = mods[mi];
|
|
803
|
+
var mpct = mod.subTaskCount > 0 ? Math.round(mod.completedSubTaskCount / mod.subTaskCount * 100) : 0;
|
|
804
|
+
html += '<div class="module-card">';
|
|
805
|
+
html += '<div class="module-card-header"><div class="module-card-dot" style="background:' + (mpct >= 100 ? '#10b981' : mpct > 0 ? '#3b82f6' : '#4b5563') + ';"></div><span class="module-card-name">' + escHtml(mod.name) + '</span></div>';
|
|
806
|
+
html += '<div class="module-card-bar"><div class="module-card-bar-fill" style="width:' + mpct + '%"></div></div>';
|
|
807
|
+
html += '<div class="module-card-stats"><span>' + mod.completedSubTaskCount + '/' + mod.subTaskCount + ' 子任务</span><span>' + mpct + '%</span></div>';
|
|
808
|
+
html += '</div>';
|
|
809
|
+
}
|
|
810
|
+
html += '</div></div>';
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ===== 文档分布 =====
|
|
814
|
+
var docSec = data.docBySection || {};
|
|
815
|
+
var docKeys = Object.keys(docSec);
|
|
816
|
+
if (docKeys.length > 0) {
|
|
817
|
+
html += '<div class="stats-section">';
|
|
818
|
+
html += '<div class="stats-section-title"><span class="sec-icon">📚</span> 文档分布</div>';
|
|
819
|
+
html += '<div class="stats-grid">';
|
|
820
|
+
var secNames = { overview: '概述', core_concepts: '核心概念', api_design: 'API 设计', file_structure: '文件结构', config: '配置', examples: '示例', technical_notes: '技术笔记', api_endpoints: 'API 端点', milestones: '里程碑', changelog: '变更日志', custom: '自定义' };
|
|
821
|
+
for (var si = 0; si < docKeys.length; si++) {
|
|
822
|
+
var sk = docKeys[si];
|
|
823
|
+
html += '<div class="stat-card purple" style="padding:14px;">';
|
|
824
|
+
html += '<div style="font-size:20px;font-weight:800;color:#a5b4fc;">' + docSec[sk] + '</div>';
|
|
825
|
+
html += '<div style="font-size:11px;color:#9ca3af;margin-top:4px;">' + (secNames[sk] || sk) + '</div>';
|
|
826
|
+
html += '</div>';
|
|
827
|
+
}
|
|
828
|
+
html += '</div></div>';
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
container.innerHTML = html;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function statCard(icon, value, label, sub, color) {
|
|
835
|
+
return '<div class="stat-card ' + color + '"><div class="stat-card-icon">' + icon + '</div><div class="stat-card-value">' + value + '</div><div class="stat-card-label">' + label + '</div>' + (sub ? '<div class="stat-card-sub">' + sub + '</div>' : '') + '</div>';
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function phaseItem(task, status, icon) {
|
|
839
|
+
var ppct = task.percent || 0;
|
|
840
|
+
var subText = task.total !== undefined ? (task.completed || 0) + '/' + task.total + ' 子任务' : task.taskId;
|
|
841
|
+
var subs = task.subTasks || [];
|
|
842
|
+
var rDocsCheck = task.relatedDocs || [];
|
|
843
|
+
var hasSubs = subs.length > 0 || rDocsCheck.length > 0;
|
|
844
|
+
var subIcons = { completed: '✓', in_progress: '◉', pending: '○', cancelled: '⊘' };
|
|
845
|
+
var mainTime = task.completedAt ? fmtTime(task.completedAt) : '';
|
|
846
|
+
var h = '<div class="phase-item-wrap">';
|
|
847
|
+
h += '<div class="phase-item-main" ' + (hasSubs ? 'onclick="togglePhaseExpand(this)"' : '') + '>';
|
|
848
|
+
if (hasSubs) { h += '<div class="phase-expand-icon">▶</div>'; }
|
|
849
|
+
h += '<div class="phase-status-icon ' + status + '">' + icon + '</div>';
|
|
850
|
+
h += '<div class="phase-info" style="flex:1;min-width:0;"><div class="phase-info-title">' + escHtml(task.title) + '</div>';
|
|
851
|
+
h += '<div class="phase-info-sub">' + escHtml(task.taskId) + ' · ' + subText;
|
|
852
|
+
if (mainTime) { h += ' · <span class="phase-time">✓ ' + mainTime + '</span>'; }
|
|
853
|
+
h += '</div></div>';
|
|
854
|
+
h += '<div class="phase-bar-mini"><div class="phase-bar-mini-fill" style="width:' + ppct + '%"></div></div>';
|
|
855
|
+
h += '<div class="phase-pct">' + ppct + '%</div>';
|
|
856
|
+
h += '</div>';
|
|
857
|
+
var rDocs = task.relatedDocs || [];
|
|
858
|
+
if (hasSubs || rDocs.length > 0) {
|
|
859
|
+
h += '<div class="phase-subtasks">';
|
|
860
|
+
for (var si = 0; si < subs.length; si++) {
|
|
861
|
+
var s = subs[si];
|
|
862
|
+
var ss = s.status || 'pending';
|
|
863
|
+
var subTime = s.completedAt ? fmtTime(s.completedAt) : '';
|
|
864
|
+
h += '<div class="phase-sub-item">';
|
|
865
|
+
h += '<div class="phase-sub-icon ' + ss + '">' + (subIcons[ss] || '○') + '</div>';
|
|
866
|
+
h += '<span class="phase-sub-name ' + ss + '">' + escHtml(s.title) + '</span>';
|
|
867
|
+
if (subTime) { h += '<span class="phase-sub-time">' + subTime + '</span>'; }
|
|
868
|
+
h += '<span class="phase-sub-id">' + escHtml(s.taskId) + '</span>';
|
|
869
|
+
h += '</div>';
|
|
870
|
+
}
|
|
871
|
+
if (rDocs.length > 0) {
|
|
872
|
+
h += '<div style="padding:6px 0 2px 8px;font-size:11px;color:#f59e0b;font-weight:600;">关联文档</div>';
|
|
873
|
+
for (var rd = 0; rd < rDocs.length; rd++) {
|
|
874
|
+
var rdoc = rDocs[rd];
|
|
875
|
+
var rdLabel = rdoc.section || '';
|
|
876
|
+
if (rdoc.subSection) rdLabel += ' / ' + rdoc.subSection;
|
|
877
|
+
h += '<div class="phase-sub-item">';
|
|
878
|
+
h += '<div class="phase-sub-icon" style="color:#f59e0b;">📄</div>';
|
|
879
|
+
h += '<span class="phase-sub-name">' + escHtml(rdoc.title) + '</span>';
|
|
880
|
+
h += '<span class="phase-sub-id">' + escHtml(rdLabel) + '</span>';
|
|
881
|
+
h += '</div>';
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
h += '</div>';
|
|
885
|
+
}
|
|
886
|
+
h += '</div>';
|
|
887
|
+
return h;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ========== Memory Browser ==========
|
|
891
|
+
var memoryLoaded = false;
|
|
892
|
+
var memoryData = [];
|
|
893
|
+
var memoryFilterType = 'all';
|
|
894
|
+
|
|
895
|
+
var MEMORY_TYPE_ICONS = {
|
|
896
|
+
decision: '🏗️',
|
|
897
|
+
bugfix: '🐛',
|
|
898
|
+
pattern: '📐',
|
|
899
|
+
insight: '💡',
|
|
900
|
+
preference: '⚙️',
|
|
901
|
+
summary: '📝'
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
var MEMORY_TYPE_LABELS = {
|
|
905
|
+
decision: '决策',
|
|
906
|
+
bugfix: 'Bug 修复',
|
|
907
|
+
pattern: '模式',
|
|
908
|
+
insight: '洞察',
|
|
909
|
+
preference: '偏好',
|
|
910
|
+
summary: '摘要'
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
function loadMemoryPage() {
|
|
914
|
+
var list = document.getElementById('memoryList');
|
|
915
|
+
if (memoryLoaded && memoryData.length > 0) {
|
|
916
|
+
renderMemoryList(memoryData);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (list) list.innerHTML = '<div style="text-align:center;padding:60px;color:#6b7280;font-size:12px;"><div class="spinner" style="margin:0 auto 12px;width:24px;height:24px;border-width:3px;"></div>加载记忆数据...</div>';
|
|
920
|
+
|
|
921
|
+
fetch('/api/memories').then(function(r) { return r.json(); }).then(function(data) {
|
|
922
|
+
memoryData = data.memories || [];
|
|
923
|
+
memoryLoaded = true;
|
|
924
|
+
renderMemoryList(memoryData);
|
|
925
|
+
}).catch(function(err) {
|
|
926
|
+
if (list) list.innerHTML = '<div style="text-align:center;padding:60px;color:#f87171;font-size:12px;">加载失败: ' + (err.message || err) + '<br><span style="cursor:pointer;color:#818cf8;text-decoration:underline;" onclick="memoryLoaded=false;loadMemoryPage();">重试</span></div>';
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function filterMemories(type) {
|
|
931
|
+
memoryFilterType = type;
|
|
932
|
+
// update button active states
|
|
933
|
+
var btns = document.querySelectorAll('.memory-filter-btn');
|
|
934
|
+
for (var i = 0; i < btns.length; i++) {
|
|
935
|
+
btns[i].classList.remove('active');
|
|
936
|
+
if (btns[i].getAttribute('data-type') === type) btns[i].classList.add('active');
|
|
937
|
+
}
|
|
938
|
+
renderMemoryList(memoryData);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function renderMemoryList(data) {
|
|
942
|
+
var list = document.getElementById('memoryList');
|
|
943
|
+
var countEl = document.getElementById('memoryCount');
|
|
944
|
+
if (!list) return;
|
|
945
|
+
|
|
946
|
+
// filter
|
|
947
|
+
var filtered = data;
|
|
948
|
+
if (memoryFilterType !== 'all') {
|
|
949
|
+
filtered = [];
|
|
950
|
+
for (var i = 0; i < data.length; i++) {
|
|
951
|
+
if (data[i].memoryType === memoryFilterType) filtered.push(data[i]);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (countEl) {
|
|
956
|
+
countEl.textContent = filtered.length + ' / ' + data.length + ' 条记忆';
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (filtered.length === 0) {
|
|
960
|
+
list.innerHTML = '<div class="memory-empty"><div class="memory-empty-icon">🧠</div>' +
|
|
961
|
+
(data.length === 0 ? '还没有保存任何记忆<br><span style="color:#4b5563;font-size:11px;margin-top:8px;display:block;">Cursor 在开发过程中会自动积累决策、Bug 修复、代码模式等知识</span>' : '没有 "' + (MEMORY_TYPE_LABELS[memoryFilterType] || memoryFilterType) + '" 类型的记忆') +
|
|
962
|
+
'</div>';
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// sort by importance desc, then by createdAt desc
|
|
967
|
+
filtered.sort(function(a, b) {
|
|
968
|
+
var ia = (a.importance || 0.5);
|
|
969
|
+
var ib = (b.importance || 0.5);
|
|
970
|
+
if (ib !== ia) return ib - ia;
|
|
971
|
+
return (b.createdAt || 0) - (a.createdAt || 0);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
var h = '';
|
|
975
|
+
for (var i = 0; i < filtered.length; i++) {
|
|
976
|
+
var mem = filtered[i];
|
|
977
|
+
var type = mem.memoryType || 'insight';
|
|
978
|
+
var icon = MEMORY_TYPE_ICONS[type] || '💭';
|
|
979
|
+
var importance = mem.importance || 0.5;
|
|
980
|
+
var pct = Math.round(importance * 100);
|
|
981
|
+
|
|
982
|
+
h += '<div class="memory-card type-' + type + '">';
|
|
983
|
+
|
|
984
|
+
// header
|
|
985
|
+
h += '<div class="memory-card-header">';
|
|
986
|
+
h += '<span class="memory-type-badge ' + type + '">' + icon + ' ' + (MEMORY_TYPE_LABELS[type] || type) + '</span>';
|
|
987
|
+
if (mem.relatedTaskId) {
|
|
988
|
+
h += '<span style="font-size:10px;color:#4b5563;background:#111827;padding:2px 6px;border-radius:3px;">' + escHtml(mem.relatedTaskId) + '</span>';
|
|
989
|
+
}
|
|
990
|
+
h += '<span class="memory-importance">';
|
|
991
|
+
h += '<span class="memory-importance-bar"><span class="memory-importance-fill" style="width:' + pct + '%"></span></span>';
|
|
992
|
+
h += ' ' + pct + '%';
|
|
993
|
+
h += '</span>';
|
|
994
|
+
h += '</div>';
|
|
995
|
+
|
|
996
|
+
// content
|
|
997
|
+
h += '<div class="memory-card-content">' + escHtml(mem.content || '') + '</div>';
|
|
998
|
+
|
|
999
|
+
// footer: tags + meta
|
|
1000
|
+
h += '<div class="memory-card-footer">';
|
|
1001
|
+
var tags = mem.tags || [];
|
|
1002
|
+
for (var t = 0; t < tags.length; t++) {
|
|
1003
|
+
h += '<span class="memory-tag">#' + escHtml(tags[t]) + '</span>';
|
|
1004
|
+
}
|
|
1005
|
+
h += '<span class="memory-meta">';
|
|
1006
|
+
if (mem.hitCount > 0) {
|
|
1007
|
+
h += '<span title="被召回次数">🔍 ' + mem.hitCount + '</span>';
|
|
1008
|
+
}
|
|
1009
|
+
if (mem.createdAt) {
|
|
1010
|
+
h += '<span>' + formatMemoryTime(mem.createdAt) + '</span>';
|
|
1011
|
+
}
|
|
1012
|
+
h += '</span>';
|
|
1013
|
+
h += '</div>';
|
|
1014
|
+
|
|
1015
|
+
h += '</div>';
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
list.innerHTML = h;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function formatMemoryTime(ts) {
|
|
1022
|
+
try {
|
|
1023
|
+
var d = new Date(ts);
|
|
1024
|
+
var now = new Date();
|
|
1025
|
+
var diff = now.getTime() - d.getTime();
|
|
1026
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前';
|
|
1027
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前';
|
|
1028
|
+
if (diff < 604800000) return Math.floor(diff / 86400000) + ' 天前';
|
|
1029
|
+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
1030
|
+
} catch(e) { return ''; }
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ========== Memory View Switch (List / 3D Graph) ==========
|
|
1034
|
+
var memoryViewMode = 'list';
|
|
1035
|
+
var memoryGraph3dInstance = null;
|
|
1036
|
+
var memoryGraphLoaded = false;
|
|
1037
|
+
var memoryGraphData = null;
|
|
1038
|
+
|
|
1039
|
+
function switchMemoryView(mode) {
|
|
1040
|
+
if (mode === memoryViewMode) return;
|
|
1041
|
+
memoryViewMode = mode;
|
|
1042
|
+
|
|
1043
|
+
// Toggle button active state
|
|
1044
|
+
var btns = document.querySelectorAll('.memory-view-btn');
|
|
1045
|
+
for (var i = 0; i < btns.length; i++) {
|
|
1046
|
+
btns[i].classList.remove('active');
|
|
1047
|
+
if (btns[i].getAttribute('data-view') === mode) btns[i].classList.add('active');
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
var listEl = document.getElementById('memoryList');
|
|
1051
|
+
var filtersEl = document.getElementById('memoryFilters');
|
|
1052
|
+
var graphEl = document.getElementById('memoryGraphContainer');
|
|
1053
|
+
var genGroup = document.querySelector('.memory-generate-group');
|
|
1054
|
+
|
|
1055
|
+
if (mode === 'graph') {
|
|
1056
|
+
if (listEl) listEl.style.display = 'none';
|
|
1057
|
+
if (filtersEl) filtersEl.style.display = 'none';
|
|
1058
|
+
if (graphEl) graphEl.style.display = 'block';
|
|
1059
|
+
if (genGroup) genGroup.style.display = 'none';
|
|
1060
|
+
loadMemoryGraph();
|
|
1061
|
+
} else {
|
|
1062
|
+
if (listEl) listEl.style.display = 'flex';
|
|
1063
|
+
if (filtersEl) filtersEl.style.display = 'flex';
|
|
1064
|
+
if (graphEl) graphEl.style.display = 'none';
|
|
1065
|
+
if (genGroup) genGroup.style.display = 'flex';
|
|
1066
|
+
// Destroy 3D graph to free memory
|
|
1067
|
+
if (memoryGraph3dInstance) {
|
|
1068
|
+
try { memoryGraph3dInstance._destructor && memoryGraph3dInstance._destructor(); } catch(e) {}
|
|
1069
|
+
memoryGraph3dInstance = null;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function loadMemoryGraph() {
|
|
1075
|
+
var loadingEl = document.getElementById('memoryGraphLoading');
|
|
1076
|
+
var graph3dEl = document.getElementById('memoryGraph3D');
|
|
1077
|
+
var statsEl = document.getElementById('memoryGraphStats');
|
|
1078
|
+
|
|
1079
|
+
if (loadingEl) loadingEl.style.display = 'block';
|
|
1080
|
+
if (graph3dEl) graph3dEl.innerHTML = '';
|
|
1081
|
+
|
|
1082
|
+
fetch('/api/memories/graph').then(function(r) { return r.json(); }).then(function(data) {
|
|
1083
|
+
if (data.error) {
|
|
1084
|
+
if (loadingEl) loadingEl.innerHTML = '<div style="color:#f87171;">加载失败: ' + data.error + '</div>';
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
memoryGraphData = data;
|
|
1088
|
+
memoryGraphLoaded = true;
|
|
1089
|
+
|
|
1090
|
+
if (statsEl) {
|
|
1091
|
+
var s = data.stats || {};
|
|
1092
|
+
statsEl.innerHTML = '🧠 ' + (s.memoryCount || 0) + ' 记忆 · ' + (s.contextCount || 0) + ' 上下文 · ' + (s.edgeCount || 0) + ' 关系';
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
renderMemoryGraph3D(data);
|
|
1096
|
+
}).catch(function(err) {
|
|
1097
|
+
if (loadingEl) loadingEl.innerHTML = '<div style="color:#f87171;">加载失败: ' + (err.message || err) + '<br><span style="cursor:pointer;color:#818cf8;text-decoration:underline;" onclick="loadMemoryGraph();">重试</span></div>';
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function renderMemoryGraph3D(data) {
|
|
1102
|
+
var container = document.getElementById('memoryGraph3D');
|
|
1103
|
+
var loadingEl = document.getElementById('memoryGraphLoading');
|
|
1104
|
+
if (!container) return;
|
|
1105
|
+
|
|
1106
|
+
// Check if ForceGraph3D is available
|
|
1107
|
+
if (typeof ForceGraph3D === 'undefined') {
|
|
1108
|
+
// Fallback message
|
|
1109
|
+
if (loadingEl) loadingEl.innerHTML = '<div style="color:#f59e0b;">3D 引擎未加载(需要 Three.js + 3D Force Graph)<br><span style="font-size:11px;color:#6b7280;">请在设置中切换到 3D 引擎后刷新页面</span></div>';
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (loadingEl) loadingEl.style.display = 'none';
|
|
1114
|
+
|
|
1115
|
+
// Node color map
|
|
1116
|
+
var NODE_COLORS = {
|
|
1117
|
+
'memory': '#c026d3',
|
|
1118
|
+
'main-task': '#3b82f6',
|
|
1119
|
+
'sub-task': '#818cf8',
|
|
1120
|
+
'doc': '#60a5fa',
|
|
1121
|
+
'module': '#ff8533',
|
|
1122
|
+
'project': '#6366f1'
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
// Edge color map
|
|
1126
|
+
var EDGE_COLORS = {
|
|
1127
|
+
'memory_relates': '#d946ef',
|
|
1128
|
+
'memory_from_task': '#60a5fa',
|
|
1129
|
+
'memory_from_doc': '#38bdf8',
|
|
1130
|
+
'module_memory': '#fb923c',
|
|
1131
|
+
'has_memory': '#a78bfa',
|
|
1132
|
+
'memory_supersedes': '#f87171'
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
// Build nodes
|
|
1136
|
+
var nodes3d = [];
|
|
1137
|
+
for (var i = 0; i < data.nodes.length; i++) {
|
|
1138
|
+
var n = data.nodes[i];
|
|
1139
|
+
var t = n.type || 'memory';
|
|
1140
|
+
var isMem = (t === 'memory');
|
|
1141
|
+
var memType = (n.properties && n.properties.memoryType) || '';
|
|
1142
|
+
var label = n.label || '';
|
|
1143
|
+
// Memory type colors
|
|
1144
|
+
var memTypeColors = {
|
|
1145
|
+
'decision': '#6366f1', 'bugfix': '#ef4444', 'pattern': '#06b6d4',
|
|
1146
|
+
'insight': '#f59e0b', 'preference': '#8b5cf6', 'summary': '#10b981'
|
|
1147
|
+
};
|
|
1148
|
+
var color = isMem ? (memTypeColors[memType] || NODE_COLORS.memory) : (NODE_COLORS[t] || '#64748b');
|
|
1149
|
+
var val = isMem ? (3 + ((n.properties && n.properties.importance) || 0.5) * 6) : (t === 'project' ? 12 : 5);
|
|
1150
|
+
|
|
1151
|
+
nodes3d.push({
|
|
1152
|
+
id: n.id,
|
|
1153
|
+
label: label,
|
|
1154
|
+
_type: t,
|
|
1155
|
+
_props: n.properties || {},
|
|
1156
|
+
_val: val,
|
|
1157
|
+
_color: color,
|
|
1158
|
+
_isMem: isMem
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Build links
|
|
1163
|
+
var links3d = [];
|
|
1164
|
+
for (var i = 0; i < data.edges.length; i++) {
|
|
1165
|
+
var e = data.edges[i];
|
|
1166
|
+
var edgeLabel = e.label || '';
|
|
1167
|
+
var edgeColor = EDGE_COLORS[edgeLabel] || '#374151';
|
|
1168
|
+
var w = (edgeLabel === 'memory_relates') ? 2 : 1;
|
|
1169
|
+
links3d.push({
|
|
1170
|
+
source: e.from,
|
|
1171
|
+
target: e.to,
|
|
1172
|
+
_label: edgeLabel,
|
|
1173
|
+
_color: edgeColor,
|
|
1174
|
+
_width: w
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Build adjacency for highlighting
|
|
1179
|
+
var _mgNeighbors = {};
|
|
1180
|
+
var _mgLinks = {};
|
|
1181
|
+
for (var i = 0; i < links3d.length; i++) {
|
|
1182
|
+
var l = links3d[i];
|
|
1183
|
+
var sId = typeof l.source === 'object' ? l.source.id : l.source;
|
|
1184
|
+
var tId = typeof l.target === 'object' ? l.target.id : l.target;
|
|
1185
|
+
if (!_mgNeighbors[sId]) _mgNeighbors[sId] = new Set();
|
|
1186
|
+
if (!_mgNeighbors[tId]) _mgNeighbors[tId] = new Set();
|
|
1187
|
+
_mgNeighbors[sId].add(tId);
|
|
1188
|
+
_mgNeighbors[tId].add(sId);
|
|
1189
|
+
if (!_mgLinks[sId]) _mgLinks[sId] = new Set();
|
|
1190
|
+
if (!_mgLinks[tId]) _mgLinks[tId] = new Set();
|
|
1191
|
+
_mgLinks[sId].add(l);
|
|
1192
|
+
_mgLinks[tId].add(l);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
var _mgSelectedId = null;
|
|
1196
|
+
var _mgHighlightNodes = new Set();
|
|
1197
|
+
var _mgHighlightLinks = new Set();
|
|
1198
|
+
|
|
1199
|
+
function updateMGHighlight(nodeId) {
|
|
1200
|
+
_mgHighlightLinks.clear();
|
|
1201
|
+
_mgHighlightNodes.clear();
|
|
1202
|
+
_mgSelectedId = nodeId;
|
|
1203
|
+
if (nodeId) {
|
|
1204
|
+
_mgHighlightNodes.add(nodeId);
|
|
1205
|
+
var nb = _mgNeighbors[nodeId];
|
|
1206
|
+
if (nb) nb.forEach(function(nId) { _mgHighlightNodes.add(nId); });
|
|
1207
|
+
var lks = _mgLinks[nodeId];
|
|
1208
|
+
if (lks) lks.forEach(function(link) { _mgHighlightLinks.add(link); });
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
var rect = container.getBoundingClientRect();
|
|
1213
|
+
|
|
1214
|
+
// Destroy previous instance
|
|
1215
|
+
if (memoryGraph3dInstance) {
|
|
1216
|
+
try { memoryGraph3dInstance._destructor && memoryGraph3dInstance._destructor(); } catch(e) {}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
var graph3d = ForceGraph3D({ controlType: 'orbit' })(container)
|
|
1220
|
+
.width(rect.width)
|
|
1221
|
+
.height(rect.height)
|
|
1222
|
+
.backgroundColor('#0f172a')
|
|
1223
|
+
.showNavInfo(false)
|
|
1224
|
+
.nodeLabel(function(n) {
|
|
1225
|
+
var memType = (n._props || {}).memoryType || '';
|
|
1226
|
+
var content = (n._props || {}).content || n.label || '';
|
|
1227
|
+
var importance = (n._props || {}).importance;
|
|
1228
|
+
var isMem = n._isMem;
|
|
1229
|
+
var typeBadge = '';
|
|
1230
|
+
if (isMem && memType) {
|
|
1231
|
+
var typeIcons = { decision:'🏗️', bugfix:'🐛', pattern:'📐', insight:'💡', preference:'⚙️', summary:'📝' };
|
|
1232
|
+
typeBadge = '<span style="font-size:10px;background:rgba(99,102,241,0.3);padding:1px 6px;border-radius:3px;">' + (typeIcons[memType] || '') + ' ' + memType + '</span>';
|
|
1233
|
+
}
|
|
1234
|
+
var impBar = '';
|
|
1235
|
+
if (isMem && importance != null) {
|
|
1236
|
+
var pct = Math.round(importance * 100);
|
|
1237
|
+
impBar = '<div style="margin-top:4px;"><span style="font-size:9px;color:#6b7280;">重要性: </span><span style="display:inline-block;width:50px;height:3px;background:#374151;border-radius:2px;vertical-align:middle;"><span style="display:block;height:100%;width:' + pct + '%;background:linear-gradient(90deg,#6366f1,#a78bfa);border-radius:2px;"></span></span> <span style="font-size:9px;color:#9ca3af;">' + pct + '%</span></div>';
|
|
1238
|
+
}
|
|
1239
|
+
return '<div style="background:rgba(15,23,42,0.92);color:#e2e8f0;padding:8px 12px;border-radius:8px;font-size:12px;border:1px solid rgba(192,38,211,0.3);backdrop-filter:blur(4px);max-width:320px;">'
|
|
1240
|
+
+ '<div style="font-weight:600;margin-bottom:3px;">' + (n.label || n.id) + '</div>'
|
|
1241
|
+
+ (typeBadge ? '<div style="margin-bottom:3px;">' + typeBadge + '</div>' : '')
|
|
1242
|
+
+ (isMem && content ? '<div style="color:#94a3b8;font-size:11px;line-height:1.4;max-height:80px;overflow:hidden;">' + content + '</div>' : '')
|
|
1243
|
+
+ impBar
|
|
1244
|
+
+ '<div style="color:#4b5563;font-size:9px;margin-top:3px;">' + (n._type || '') + '</div>'
|
|
1245
|
+
+ '</div>';
|
|
1246
|
+
})
|
|
1247
|
+
.nodeColor(function(n) { return n._color; })
|
|
1248
|
+
.nodeVal(function(n) { return n._val; })
|
|
1249
|
+
.nodeOpacity(0.92)
|
|
1250
|
+
.nodeResolution(16)
|
|
1251
|
+
.nodeThreeObject(function(n) {
|
|
1252
|
+
if (typeof THREE === 'undefined') return false;
|
|
1253
|
+
var color = n._color;
|
|
1254
|
+
var group = new THREE.Group();
|
|
1255
|
+
var coreMesh;
|
|
1256
|
+
|
|
1257
|
+
if (n._isMem) {
|
|
1258
|
+
// Memory nodes: dodecahedron (多面体)
|
|
1259
|
+
var size = 2 + (n._val || 5) * 0.5;
|
|
1260
|
+
var geo = new THREE.DodecahedronGeometry(size);
|
|
1261
|
+
var mat = new THREE.MeshLambertMaterial({ color: color, transparent: true, opacity: 0.92, emissive: color, emissiveIntensity: 0.35 });
|
|
1262
|
+
coreMesh = new THREE.Mesh(geo, mat);
|
|
1263
|
+
} else if (n._type === 'project') {
|
|
1264
|
+
var geo = new THREE.OctahedronGeometry(10);
|
|
1265
|
+
var mat = new THREE.MeshLambertMaterial({ color: color, transparent: true, opacity: 0.92, emissive: color, emissiveIntensity: 0.4 });
|
|
1266
|
+
coreMesh = new THREE.Mesh(geo, mat);
|
|
1267
|
+
} else if (n._type === 'module') {
|
|
1268
|
+
var size = 6;
|
|
1269
|
+
var geo = new THREE.BoxGeometry(size, size, size);
|
|
1270
|
+
var mat = new THREE.MeshLambertMaterial({ color: color, transparent: true, opacity: 0.92, emissive: color, emissiveIntensity: 0.3 });
|
|
1271
|
+
coreMesh = new THREE.Mesh(geo, mat);
|
|
1272
|
+
} else {
|
|
1273
|
+
// Tasks, docs: sphere
|
|
1274
|
+
var radius = 3 + (n._val || 5) * 0.2;
|
|
1275
|
+
var geo = new THREE.SphereGeometry(radius, 12, 12);
|
|
1276
|
+
var mat = new THREE.MeshLambertMaterial({ color: color, transparent: true, opacity: 0.85, emissive: color, emissiveIntensity: 0.2 });
|
|
1277
|
+
coreMesh = new THREE.Mesh(geo, mat);
|
|
1278
|
+
}
|
|
1279
|
+
group.add(coreMesh);
|
|
1280
|
+
|
|
1281
|
+
// Glow sprite for memory nodes
|
|
1282
|
+
if (n._isMem) {
|
|
1283
|
+
var spriteMat = new THREE.SpriteMaterial({
|
|
1284
|
+
map: createGlowTexture_mg(color),
|
|
1285
|
+
transparent: true,
|
|
1286
|
+
opacity: 0.4,
|
|
1287
|
+
depthWrite: false,
|
|
1288
|
+
blending: THREE.AdditiveBlending
|
|
1289
|
+
});
|
|
1290
|
+
var sprite = new THREE.Sprite(spriteMat);
|
|
1291
|
+
var spriteSize = (n._val || 5) * 2.5;
|
|
1292
|
+
sprite.scale.set(spriteSize, spriteSize, 1);
|
|
1293
|
+
group.add(sprite);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
return group;
|
|
1297
|
+
})
|
|
1298
|
+
.nodeThreeObjectExtend(false)
|
|
1299
|
+
// Link styles
|
|
1300
|
+
.linkColor(function(l) {
|
|
1301
|
+
if (_mgSelectedId && !_mgHighlightLinks.has(l)) return 'rgba(55,65,81,0.15)';
|
|
1302
|
+
return l._color;
|
|
1303
|
+
})
|
|
1304
|
+
.linkWidth(function(l) {
|
|
1305
|
+
return _mgHighlightLinks.has(l) ? l._width * 2 : l._width;
|
|
1306
|
+
})
|
|
1307
|
+
.linkOpacity(0.7)
|
|
1308
|
+
.linkDirectionalParticles(function(l) {
|
|
1309
|
+
return _mgHighlightLinks.has(l) ? 3 : 0;
|
|
1310
|
+
})
|
|
1311
|
+
.linkDirectionalParticleWidth(2)
|
|
1312
|
+
.linkDirectionalParticleSpeed(0.006)
|
|
1313
|
+
.linkDirectionalParticleColor(function(l) { return l._color; })
|
|
1314
|
+
// Interactions
|
|
1315
|
+
.onNodeClick(function(node) {
|
|
1316
|
+
if (_mgSelectedId === node.id) {
|
|
1317
|
+
updateMGHighlight(null);
|
|
1318
|
+
} else {
|
|
1319
|
+
updateMGHighlight(node.id);
|
|
1320
|
+
}
|
|
1321
|
+
graph3d.nodeColor(graph3d.nodeColor()); // trigger refresh
|
|
1322
|
+
|
|
1323
|
+
// Show detail in panel if available
|
|
1324
|
+
if (typeof showPanel === 'function') {
|
|
1325
|
+
var panelNode = {
|
|
1326
|
+
id: node.id,
|
|
1327
|
+
label: node.label,
|
|
1328
|
+
_type: node._type,
|
|
1329
|
+
_props: node._props
|
|
1330
|
+
};
|
|
1331
|
+
showPanel(panelNode);
|
|
1332
|
+
}
|
|
1333
|
+
})
|
|
1334
|
+
.onBackgroundClick(function() {
|
|
1335
|
+
updateMGHighlight(null);
|
|
1336
|
+
graph3d.nodeColor(graph3d.nodeColor());
|
|
1337
|
+
if (typeof closePanel === 'function') closePanel();
|
|
1338
|
+
})
|
|
1339
|
+
.graphData({ nodes: nodes3d, links: links3d });
|
|
1340
|
+
|
|
1341
|
+
// Force simulation tuning for memory network
|
|
1342
|
+
graph3d.d3Force('charge').strength(-120).distanceMax(300);
|
|
1343
|
+
graph3d.d3Force('link').distance(function(l) {
|
|
1344
|
+
return l._label === 'memory_relates' ? 40 : 80;
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
memoryGraph3dInstance = graph3d;
|
|
1348
|
+
|
|
1349
|
+
// Auto-zoom to fit
|
|
1350
|
+
setTimeout(function() {
|
|
1351
|
+
try { graph3d.zoomToFit(800, 40); } catch(e) {}
|
|
1352
|
+
}, 1500);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Glow texture generator for memory nodes
|
|
1356
|
+
function createGlowTexture_mg(colorHex) {
|
|
1357
|
+
if (typeof document === 'undefined') return null;
|
|
1358
|
+
var size = 128;
|
|
1359
|
+
var canvas = document.createElement('canvas');
|
|
1360
|
+
canvas.width = size;
|
|
1361
|
+
canvas.height = size;
|
|
1362
|
+
var ctx = canvas.getContext('2d');
|
|
1363
|
+
if (!ctx) return null;
|
|
1364
|
+
var gradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
1365
|
+
gradient.addColorStop(0, colorHex);
|
|
1366
|
+
gradient.addColorStop(0.3, colorHex + 'aa');
|
|
1367
|
+
gradient.addColorStop(0.7, colorHex + '33');
|
|
1368
|
+
gradient.addColorStop(1, 'transparent');
|
|
1369
|
+
ctx.fillStyle = gradient;
|
|
1370
|
+
ctx.fillRect(0, 0, size, size);
|
|
1371
|
+
var tex = new THREE.CanvasTexture(canvas);
|
|
1372
|
+
return tex;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// ========== Memory Generate ==========
|
|
1376
|
+
var memGenCandidates = [];
|
|
1377
|
+
var memGenSelected = {};
|
|
1378
|
+
var memGenLastSource = 'both';
|
|
1379
|
+
var memGenLastTaskId = null;
|
|
1380
|
+
var memGenTotalSaved = 0;
|
|
1381
|
+
|
|
1382
|
+
function toggleMemGenDropdown(e) {
|
|
1383
|
+
if (e) e.stopPropagation();
|
|
1384
|
+
var dd = document.getElementById('memGenDropdown');
|
|
1385
|
+
if (dd) dd.classList.toggle('show');
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// close dropdown on body click
|
|
1389
|
+
document.addEventListener('click', function(e) {
|
|
1390
|
+
var dd = document.getElementById('memGenDropdown');
|
|
1391
|
+
if (dd && dd.classList.contains('show')) {
|
|
1392
|
+
var group = dd.closest('.memory-generate-group');
|
|
1393
|
+
if (group && !group.contains(e.target)) dd.classList.remove('show');
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
function getMemGenLimit() {
|
|
1398
|
+
var sel = document.getElementById('memGenLimitSelect');
|
|
1399
|
+
var v = sel ? parseInt(sel.value, 10) : 50;
|
|
1400
|
+
return v === 0 ? 99999 : v;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function isAutoNextEnabled() {
|
|
1404
|
+
var cb = document.getElementById('memGenAutoNext');
|
|
1405
|
+
return cb ? cb.checked : false;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function onMemGenLimitChange() {
|
|
1409
|
+
// When user changes limit, toggle auto-next visibility hint
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function generateMemories(source, taskId) {
|
|
1413
|
+
// close dropdown
|
|
1414
|
+
var dd = document.getElementById('memGenDropdown');
|
|
1415
|
+
if (dd) dd.classList.remove('show');
|
|
1416
|
+
|
|
1417
|
+
// track source for auto-continue
|
|
1418
|
+
memGenLastSource = source || 'both';
|
|
1419
|
+
memGenLastTaskId = taskId || null;
|
|
1420
|
+
memGenTotalSaved = 0;
|
|
1421
|
+
|
|
1422
|
+
// show overlay
|
|
1423
|
+
var overlay = document.getElementById('memGenOverlay');
|
|
1424
|
+
if (overlay) overlay.style.display = 'flex';
|
|
1425
|
+
|
|
1426
|
+
loadMemGenBatch();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function loadMemGenBatch() {
|
|
1430
|
+
var listEl = document.getElementById('memGenCandidateList');
|
|
1431
|
+
var limit = getMemGenLimit();
|
|
1432
|
+
var sourceLabel = memGenLastSource === 'both' ? '文档+任务' : memGenLastSource === 'tasks' ? '任务' : '文档';
|
|
1433
|
+
var batchInfo = memGenTotalSaved > 0 ? '(已累计保存 ' + memGenTotalSaved + ' 条,加载下一批...)' : '';
|
|
1434
|
+
if (listEl) listEl.innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;font-size:12px;"><div class="spinner" style="margin:0 auto 12px;width:24px;height:24px;border-width:3px;"></div>正在从 ' + sourceLabel + ' 中提取候选项...' + batchInfo + '</div>';
|
|
1435
|
+
|
|
1436
|
+
var url = '/api/memories/generate?source=' + encodeURIComponent(memGenLastSource) + '&limit=' + limit;
|
|
1437
|
+
if (memGenLastTaskId) url += '&taskId=' + encodeURIComponent(memGenLastTaskId);
|
|
1438
|
+
|
|
1439
|
+
// reset save button
|
|
1440
|
+
var btn = document.getElementById('memGenSaveBtn');
|
|
1441
|
+
if (btn) { btn.disabled = false; btn.innerHTML = '💾 保存选中 (<span id="memGenSelectedCount">0</span>)'; }
|
|
1442
|
+
|
|
1443
|
+
fetch(url).then(function(r) { return r.json(); }).then(function(data) {
|
|
1444
|
+
memGenCandidates = data.candidates || [];
|
|
1445
|
+
memGenSelected = {};
|
|
1446
|
+
// auto-select all candidates (已有记忆的候选项已被服务端过滤)
|
|
1447
|
+
for (var i = 0; i < memGenCandidates.length; i++) {
|
|
1448
|
+
memGenSelected[i] = true;
|
|
1449
|
+
}
|
|
1450
|
+
renderCandidateList();
|
|
1451
|
+
updateGenStats(data);
|
|
1452
|
+
}).catch(function(err) {
|
|
1453
|
+
if (listEl) listEl.innerHTML = '<div style="text-align:center;padding:40px;color:#f87171;font-size:12px;">生成失败: ' + (err.message || err) + '</div>';
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function closeMemGenOverlay() {
|
|
1458
|
+
var overlay = document.getElementById('memGenOverlay');
|
|
1459
|
+
if (overlay) overlay.style.display = 'none';
|
|
1460
|
+
memGenCandidates = [];
|
|
1461
|
+
memGenSelected = {};
|
|
1462
|
+
memGenTotalSaved = 0;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function updateGenStats(data) {
|
|
1466
|
+
var statsEl = document.getElementById('memGenStats');
|
|
1467
|
+
if (statsEl) {
|
|
1468
|
+
var total = (data.candidates || []).length;
|
|
1469
|
+
var fromTasks = 0, fromDocs = 0;
|
|
1470
|
+
for (var i = 0; i < total; i++) {
|
|
1471
|
+
if (data.candidates[i].sourceType === 'task') fromTasks++;
|
|
1472
|
+
else fromDocs++;
|
|
1473
|
+
}
|
|
1474
|
+
var skipped = (data.stats && data.stats.skippedWithMemory) || 0;
|
|
1475
|
+
var txt = '共 ' + total + ' 条 (任务: ' + fromTasks + ', 文档: ' + fromDocs + ')';
|
|
1476
|
+
if (skipped > 0) txt += ' · 已跳过 ' + skipped + ' 条已有记忆';
|
|
1477
|
+
if (memGenTotalSaved > 0) txt += ' · 已累计保存 ' + memGenTotalSaved + ' 条';
|
|
1478
|
+
var limit = getMemGenLimit();
|
|
1479
|
+
if (total === 0 && memGenTotalSaved > 0) {
|
|
1480
|
+
txt = '🎉 全部处理完毕!累计保存 ' + memGenTotalSaved + ' 条记忆';
|
|
1481
|
+
} else if (limit < 99999) {
|
|
1482
|
+
txt += ' (每批 ' + limit + ')';
|
|
1483
|
+
}
|
|
1484
|
+
statsEl.textContent = txt;
|
|
1485
|
+
}
|
|
1486
|
+
updateSelectedCount();
|
|
1487
|
+
// If no candidates returned and auto-next was running, auto-close
|
|
1488
|
+
if ((data.candidates || []).length === 0 && memGenTotalSaved > 0) {
|
|
1489
|
+
var btn = document.getElementById('memGenSaveBtn');
|
|
1490
|
+
if (btn) { btn.disabled = true; btn.textContent = '🎉 全部完成!共保存 ' + memGenTotalSaved + ' 条'; }
|
|
1491
|
+
memoryLoaded = false;
|
|
1492
|
+
setTimeout(function() { closeMemGenOverlay(); loadMemoryPage(); }, 2000);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function updateSelectedCount() {
|
|
1497
|
+
var cnt = 0;
|
|
1498
|
+
for (var k in memGenSelected) { if (memGenSelected[k]) cnt++; }
|
|
1499
|
+
var el = document.getElementById('memGenSelectedCount');
|
|
1500
|
+
if (el) el.textContent = cnt;
|
|
1501
|
+
var btn = document.getElementById('memGenSaveBtn');
|
|
1502
|
+
if (btn) btn.disabled = cnt === 0;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function toggleCandidate(idx) {
|
|
1506
|
+
memGenSelected[idx] = !memGenSelected[idx];
|
|
1507
|
+
// update card UI
|
|
1508
|
+
var card = document.querySelector('.mem-gen-candidate[data-idx="' + idx + '"]');
|
|
1509
|
+
if (card) {
|
|
1510
|
+
card.classList.toggle('selected', !!memGenSelected[idx]);
|
|
1511
|
+
var check = card.querySelector('.mem-gen-candidate-check');
|
|
1512
|
+
if (check) check.textContent = memGenSelected[idx] ? '✓' : '';
|
|
1513
|
+
}
|
|
1514
|
+
updateSelectedCount();
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function toggleAllCandidates(state) {
|
|
1518
|
+
// state: true=all, false=none (已有记忆的候选项已被服务端过滤,无需客户端二次判断)
|
|
1519
|
+
for (var i = 0; i < memGenCandidates.length; i++) {
|
|
1520
|
+
memGenSelected[i] = !!state;
|
|
1521
|
+
}
|
|
1522
|
+
renderCandidateList();
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function renderCandidateList() {
|
|
1526
|
+
var listEl = document.getElementById('memGenCandidateList');
|
|
1527
|
+
if (!listEl) return;
|
|
1528
|
+
|
|
1529
|
+
if (memGenCandidates.length === 0) {
|
|
1530
|
+
listEl.innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;font-size:13px;">没有找到可以提取的候选项</div>';
|
|
1531
|
+
updateSelectedCount();
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
var h = '';
|
|
1536
|
+
for (var i = 0; i < memGenCandidates.length; i++) {
|
|
1537
|
+
var c = memGenCandidates[i];
|
|
1538
|
+
var sel = !!memGenSelected[i];
|
|
1539
|
+
var cls = 'mem-gen-candidate' + (sel ? ' selected' : '');
|
|
1540
|
+
var typeIcon = MEMORY_TYPE_ICONS[c.suggestedMemoryType] || '💭';
|
|
1541
|
+
var typeLabel = MEMORY_TYPE_LABELS[c.suggestedMemoryType] || c.suggestedMemoryType;
|
|
1542
|
+
|
|
1543
|
+
h += '<div class="' + cls + '" data-idx="' + i + '" onclick="toggleCandidate(' + i + ')">';
|
|
1544
|
+
h += '<div class="mem-gen-candidate-check">' + (sel ? '✓' : '') + '</div>';
|
|
1545
|
+
h += '<div class="mem-gen-candidate-body">';
|
|
1546
|
+
|
|
1547
|
+
// title row
|
|
1548
|
+
h += '<div class="mem-gen-candidate-title">';
|
|
1549
|
+
h += '<span class="mem-gen-candidate-source ' + c.sourceType + '">' + (c.sourceType === 'task' ? '✅ 任务' : '📄 文档') + '</span>';
|
|
1550
|
+
h += escHtml(c.sourceTitle);
|
|
1551
|
+
h += '</div>';
|
|
1552
|
+
|
|
1553
|
+
// content preview (truncate)
|
|
1554
|
+
var preview = (c.content || '').substring(0, 200);
|
|
1555
|
+
if ((c.content || '').length > 200) preview += '...';
|
|
1556
|
+
h += '<div class="mem-gen-candidate-preview">' + escHtml(preview) + '</div>';
|
|
1557
|
+
|
|
1558
|
+
// meta
|
|
1559
|
+
h += '<div class="mem-gen-candidate-meta">';
|
|
1560
|
+
h += '<span class="mem-gen-candidate-type ' + c.suggestedMemoryType + '">' + typeIcon + ' ' + typeLabel + '</span>';
|
|
1561
|
+
h += '<span class="mem-gen-candidate-importance">重要性: ' + Math.round((c.suggestedImportance || 0.5) * 100) + '%</span>';
|
|
1562
|
+
h += '</div>';
|
|
1563
|
+
|
|
1564
|
+
h += '</div>';
|
|
1565
|
+
h += '</div>';
|
|
1566
|
+
}
|
|
1567
|
+
listEl.innerHTML = h;
|
|
1568
|
+
updateSelectedCount();
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function saveSelectedCandidates() {
|
|
1572
|
+
var toSave = [];
|
|
1573
|
+
for (var i = 0; i < memGenCandidates.length; i++) {
|
|
1574
|
+
if (memGenSelected[i]) toSave.push(memGenCandidates[i]);
|
|
1575
|
+
}
|
|
1576
|
+
if (toSave.length === 0) return;
|
|
1577
|
+
|
|
1578
|
+
var btn = document.getElementById('memGenSaveBtn');
|
|
1579
|
+
if (btn) { btn.disabled = true; btn.textContent = '💾 保存中...'; }
|
|
1580
|
+
|
|
1581
|
+
var saved = 0, failed = 0;
|
|
1582
|
+
var total = toSave.length;
|
|
1583
|
+
var batchLimit = getMemGenLimit();
|
|
1584
|
+
var batchSize = memGenCandidates.length;
|
|
1585
|
+
|
|
1586
|
+
function saveNext(idx) {
|
|
1587
|
+
if (idx >= total) {
|
|
1588
|
+
// batch done
|
|
1589
|
+
memGenTotalSaved += saved;
|
|
1590
|
+
var doneMsg = '✅ 本批保存 ' + saved + ' 条' + (failed > 0 ? ' (失败 ' + failed + ')' : '');
|
|
1591
|
+
if (memGenTotalSaved > saved) doneMsg += ' · 累计 ' + memGenTotalSaved + ' 条';
|
|
1592
|
+
|
|
1593
|
+
// Check if we should auto-load next batch:
|
|
1594
|
+
// - auto-next is enabled
|
|
1595
|
+
// - batch was full (batchSize >= limit), meaning there might be more
|
|
1596
|
+
// - limit is not "unlimited" (99999)
|
|
1597
|
+
var shouldContinue = isAutoNextEnabled() && batchLimit < 99999 && batchSize >= batchLimit;
|
|
1598
|
+
|
|
1599
|
+
if (shouldContinue) {
|
|
1600
|
+
if (btn) btn.textContent = doneMsg + ' — 加载下一批...';
|
|
1601
|
+
setTimeout(function() { loadMemGenBatch(); }, 800);
|
|
1602
|
+
} else {
|
|
1603
|
+
if (btn) btn.textContent = doneMsg;
|
|
1604
|
+
if (batchSize < batchLimit || batchLimit >= 99999) {
|
|
1605
|
+
if (btn) btn.textContent = '✅ 全部完成!共保存 ' + memGenTotalSaved + ' 条';
|
|
1606
|
+
}
|
|
1607
|
+
// refresh memories list
|
|
1608
|
+
memoryLoaded = false;
|
|
1609
|
+
setTimeout(function() {
|
|
1610
|
+
closeMemGenOverlay();
|
|
1611
|
+
loadMemoryPage();
|
|
1612
|
+
}, 1500);
|
|
1613
|
+
}
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
var c = toSave[idx];
|
|
1618
|
+
// Build content: use sourceTitle + content snippet
|
|
1619
|
+
var memContent = c.content || c.sourceTitle || '';
|
|
1620
|
+
if (memContent.length > 500) memContent = memContent.substring(0, 500) + '...';
|
|
1621
|
+
|
|
1622
|
+
fetch('/api/memories/save', {
|
|
1623
|
+
method: 'POST',
|
|
1624
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1625
|
+
body: JSON.stringify({
|
|
1626
|
+
memoryType: c.suggestedMemoryType || 'summary',
|
|
1627
|
+
content: memContent,
|
|
1628
|
+
tags: c.suggestedTags || [],
|
|
1629
|
+
relatedTaskId: c.sourceType === 'task' ? c.sourceId : undefined,
|
|
1630
|
+
sourceId: c.sourceId,
|
|
1631
|
+
importance: c.suggestedImportance || 0.5,
|
|
1632
|
+
})
|
|
1633
|
+
}).then(function(r) { return r.json(); }).then(function() {
|
|
1634
|
+
saved++;
|
|
1635
|
+
if (btn) btn.textContent = '💾 保存中... (' + saved + '/' + total + ')';
|
|
1636
|
+
saveNext(idx + 1);
|
|
1637
|
+
}).catch(function() {
|
|
1638
|
+
failed++;
|
|
1639
|
+
saveNext(idx + 1);
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
saveNext(0);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function showPhasePickerForGenerate() {
|
|
1647
|
+
// close dropdown first
|
|
1648
|
+
var dd = document.getElementById('memGenDropdown');
|
|
1649
|
+
if (dd) dd.classList.remove('show');
|
|
1650
|
+
|
|
1651
|
+
// fetch progress to get completed phases
|
|
1652
|
+
fetch('/api/progress').then(function(r) { return r.json(); }).then(function(data) {
|
|
1653
|
+
var phases = (data.completedPhases || []);
|
|
1654
|
+
if (phases.length === 0) {
|
|
1655
|
+
alert('没有已完成的阶段');
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
var overlay = document.getElementById('memGenOverlay');
|
|
1659
|
+
if (overlay) overlay.style.display = 'flex';
|
|
1660
|
+
var listEl = document.getElementById('memGenCandidateList');
|
|
1661
|
+
var statsEl = document.getElementById('memGenStats');
|
|
1662
|
+
if (statsEl) statsEl.textContent = '请选择一个阶段';
|
|
1663
|
+
|
|
1664
|
+
var h = '<div style="padding:8px;">';
|
|
1665
|
+
h += '<div style="font-size:13px;color:#9ca3af;margin-bottom:12px;">选择要提取记忆的阶段:</div>';
|
|
1666
|
+
for (var i = 0; i < phases.length; i++) {
|
|
1667
|
+
var p = phases[i];
|
|
1668
|
+
h += '<div class="mem-gen-candidate" onclick="generateMemories(\\'tasks\\',\\'' + escHtml(p.taskId) + '\\')" style="cursor:pointer;">';
|
|
1669
|
+
h += '<div class="mem-gen-candidate-body">';
|
|
1670
|
+
h += '<div class="mem-gen-candidate-title">';
|
|
1671
|
+
h += '<span class="mem-gen-candidate-source task">' + escHtml(p.taskId) + '</span>';
|
|
1672
|
+
h += escHtml(p.title);
|
|
1673
|
+
h += '</div>';
|
|
1674
|
+
var desc = '';
|
|
1675
|
+
if (p.completedSubTasks !== undefined) desc = '子任务: ' + p.completedSubTasks + '/' + p.totalSubTasks;
|
|
1676
|
+
h += '<div class="mem-gen-candidate-preview">' + escHtml(desc) + '</div>';
|
|
1677
|
+
h += '</div>';
|
|
1678
|
+
h += '</div>';
|
|
1679
|
+
}
|
|
1680
|
+
h += '</div>';
|
|
1681
|
+
if (listEl) listEl.innerHTML = h;
|
|
1682
|
+
}).catch(function() {
|
|
1683
|
+
alert('获取阶段列表失败');
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// ========== 一键全量导入 ==========
|
|
1688
|
+
var _autoImportCancelled = false;
|
|
1689
|
+
|
|
1690
|
+
function autoImportAllMemories() {
|
|
1691
|
+
// close dropdown
|
|
1692
|
+
var dd = document.getElementById('memGenDropdown');
|
|
1693
|
+
if (dd) dd.classList.remove('show');
|
|
1694
|
+
|
|
1695
|
+
_autoImportCancelled = false;
|
|
1696
|
+
|
|
1697
|
+
// show progress overlay
|
|
1698
|
+
var overlay = document.getElementById('memAutoImportOverlay');
|
|
1699
|
+
if (overlay) overlay.style.display = 'flex';
|
|
1700
|
+
|
|
1701
|
+
var titleEl = document.getElementById('memAutoImportTitle');
|
|
1702
|
+
var statusEl = document.getElementById('memAutoImportStatus');
|
|
1703
|
+
var detailEl = document.getElementById('memAutoImportDetail');
|
|
1704
|
+
var progressEl = document.getElementById('memAutoImportProgress');
|
|
1705
|
+
var cancelBtn = document.getElementById('memAutoImportCancelBtn');
|
|
1706
|
+
if (titleEl) titleEl.textContent = '⚡ 一键全量导入';
|
|
1707
|
+
if (statusEl) statusEl.textContent = '正在获取候选项...';
|
|
1708
|
+
if (detailEl) detailEl.textContent = '';
|
|
1709
|
+
if (progressEl) progressEl.style.width = '0%';
|
|
1710
|
+
if (cancelBtn) { cancelBtn.disabled = false; cancelBtn.textContent = '取消'; }
|
|
1711
|
+
|
|
1712
|
+
var totalSaved = 0;
|
|
1713
|
+
var totalFailed = 0;
|
|
1714
|
+
var totalSkipped = 0;
|
|
1715
|
+
var batchNum = 0;
|
|
1716
|
+
// Phase-44: Memory Tree — 记录 sourceId → entityId 映射,用于建立 suggestedRelations
|
|
1717
|
+
var sourceIdToEntityId = {};
|
|
1718
|
+
var pendingRelations = [];
|
|
1719
|
+
var totalRelationsCreated = 0;
|
|
1720
|
+
|
|
1721
|
+
function loadAndSaveBatch() {
|
|
1722
|
+
if (_autoImportCancelled) { finishImport('已取消'); return; }
|
|
1723
|
+
|
|
1724
|
+
batchNum++;
|
|
1725
|
+
if (statusEl) statusEl.textContent = '第 ' + batchNum + ' 批 — 获取候选项...';
|
|
1726
|
+
if (detailEl) detailEl.textContent = '已累计保存: ' + totalSaved + ' 条' + (totalSkipped > 0 ? ' · 跳过: ' + totalSkipped : '');
|
|
1727
|
+
|
|
1728
|
+
fetch('/api/memories/generate?source=both&limit=50').then(function(r) { return r.json(); }).then(function(data) {
|
|
1729
|
+
var candidates = data.candidates || [];
|
|
1730
|
+
var skipped = (data.stats && data.stats.skippedWithMemory) || 0;
|
|
1731
|
+
totalSkipped = skipped;
|
|
1732
|
+
|
|
1733
|
+
if (candidates.length === 0) {
|
|
1734
|
+
finishImport('完成');
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
if (statusEl) statusEl.textContent = '第 ' + batchNum + ' 批 — 保存 ' + candidates.length + ' 条...';
|
|
1739
|
+
|
|
1740
|
+
var batchSaved = 0;
|
|
1741
|
+
var batchFailed = 0;
|
|
1742
|
+
|
|
1743
|
+
function saveOne(idx) {
|
|
1744
|
+
if (_autoImportCancelled) { totalSaved += batchSaved; totalFailed += batchFailed; finishImport('已取消'); return; }
|
|
1745
|
+
if (idx >= candidates.length) {
|
|
1746
|
+
totalSaved += batchSaved;
|
|
1747
|
+
totalFailed += batchFailed;
|
|
1748
|
+
// continue to next batch
|
|
1749
|
+
setTimeout(loadAndSaveBatch, 300);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
var c = candidates[idx];
|
|
1754
|
+
var memContent = c.content || c.sourceTitle || '';
|
|
1755
|
+
if (memContent.length > 500) memContent = memContent.substring(0, 500) + '...';
|
|
1756
|
+
|
|
1757
|
+
// progress within batch
|
|
1758
|
+
var pctBatch = Math.round(((idx + 1) / candidates.length) * 100);
|
|
1759
|
+
if (progressEl) progressEl.style.width = pctBatch + '%';
|
|
1760
|
+
var relCount = (c.suggestedRelations || []).length;
|
|
1761
|
+
if (detailEl) detailEl.textContent = '批次 ' + batchNum + ': ' + (idx + 1) + '/' + candidates.length + ' · 累计保存: ' + (totalSaved + batchSaved) + ' 条' + (relCount > 0 ? ' · 关系: ' + relCount : '');
|
|
1762
|
+
|
|
1763
|
+
fetch('/api/memories/save', {
|
|
1764
|
+
method: 'POST',
|
|
1765
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1766
|
+
body: JSON.stringify({
|
|
1767
|
+
memoryType: c.suggestedMemoryType || 'summary',
|
|
1768
|
+
content: memContent,
|
|
1769
|
+
tags: c.suggestedTags || [],
|
|
1770
|
+
relatedTaskId: c.sourceType === 'task' ? c.sourceId : undefined,
|
|
1771
|
+
sourceId: c.sourceId,
|
|
1772
|
+
importance: c.suggestedImportance || 0.5,
|
|
1773
|
+
})
|
|
1774
|
+
}).then(function(r) { return r.json(); }).then(function(result) {
|
|
1775
|
+
batchSaved++;
|
|
1776
|
+
// Phase-44: 记录 sourceId → entityId 映射,用于后续建立 suggestedRelations
|
|
1777
|
+
if (result && result.memory && result.memory.id && c.sourceId) {
|
|
1778
|
+
sourceIdToEntityId[c.sourceId] = result.memory.id;
|
|
1779
|
+
}
|
|
1780
|
+
// 收集 suggestedRelations 待后续处理
|
|
1781
|
+
if (c.suggestedRelations && c.suggestedRelations.length > 0) {
|
|
1782
|
+
pendingRelations.push({ sourceId: c.sourceId, relations: c.suggestedRelations });
|
|
1783
|
+
}
|
|
1784
|
+
saveOne(idx + 1);
|
|
1785
|
+
}).catch(function() {
|
|
1786
|
+
batchFailed++;
|
|
1787
|
+
saveOne(idx + 1);
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
saveOne(0);
|
|
1792
|
+
}).catch(function(err) {
|
|
1793
|
+
if (statusEl) statusEl.textContent = '获取候选项失败';
|
|
1794
|
+
if (detailEl) detailEl.textContent = err.message || String(err);
|
|
1795
|
+
if (cancelBtn) { cancelBtn.textContent = '关闭'; cancelBtn.disabled = false; }
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function finishImport(reason) {
|
|
1800
|
+
if (progressEl) progressEl.style.width = '100%';
|
|
1801
|
+
if (reason === '已取消') {
|
|
1802
|
+
if (statusEl) statusEl.textContent = '已取消 — 保存了 ' + totalSaved + ' 条';
|
|
1803
|
+
if (titleEl) titleEl.textContent = '⚡ 导入已取消';
|
|
1804
|
+
showFinalStats();
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// Phase-44: 第二阶段 — 用 suggestedRelations 建立记忆间关系
|
|
1809
|
+
if (pendingRelations.length > 0) {
|
|
1810
|
+
if (statusEl) statusEl.textContent = '🔗 正在建立记忆关系...';
|
|
1811
|
+
if (detailEl) detailEl.textContent = '已保存 ' + totalSaved + ' 条记忆,正在处理 ' + pendingRelations.length + ' 组关系建议';
|
|
1812
|
+
|
|
1813
|
+
var relIdx = 0;
|
|
1814
|
+
function processNextRelation() {
|
|
1815
|
+
if (relIdx >= pendingRelations.length) {
|
|
1816
|
+
if (titleEl) titleEl.textContent = '✅ 导入完成(含记忆树)';
|
|
1817
|
+
if (statusEl) statusEl.textContent = '🎉 记忆 + 关系全部导入完成!';
|
|
1818
|
+
showFinalStats();
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
var item = pendingRelations[relIdx];
|
|
1822
|
+
var fromEntityId = sourceIdToEntityId[item.sourceId];
|
|
1823
|
+
if (!fromEntityId) { relIdx++; processNextRelation(); return; }
|
|
1824
|
+
|
|
1825
|
+
var rels = item.relations || [];
|
|
1826
|
+
var rIdx = 0;
|
|
1827
|
+
function createNextRel() {
|
|
1828
|
+
if (rIdx >= rels.length) { relIdx++; processNextRelation(); return; }
|
|
1829
|
+
var rel = rels[rIdx];
|
|
1830
|
+
var toEntityId = sourceIdToEntityId[rel.targetSourceId];
|
|
1831
|
+
if (!toEntityId) { rIdx++; createNextRel(); return; }
|
|
1832
|
+
|
|
1833
|
+
fetch('/api/memories/relate', {
|
|
1834
|
+
method: 'POST',
|
|
1835
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1836
|
+
body: JSON.stringify({
|
|
1837
|
+
fromId: fromEntityId,
|
|
1838
|
+
toId: toEntityId,
|
|
1839
|
+
relationType: rel.relationType || 'MEMORY_RELATES',
|
|
1840
|
+
weight: rel.weight || 0.5,
|
|
1841
|
+
})
|
|
1842
|
+
}).then(function() {
|
|
1843
|
+
totalRelationsCreated++;
|
|
1844
|
+
if (detailEl) detailEl.textContent = '关系: ' + totalRelationsCreated + ' 条已建立';
|
|
1845
|
+
rIdx++;
|
|
1846
|
+
createNextRel();
|
|
1847
|
+
}).catch(function() {
|
|
1848
|
+
rIdx++;
|
|
1849
|
+
createNextRel();
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
createNextRel();
|
|
1853
|
+
}
|
|
1854
|
+
processNextRelation();
|
|
1855
|
+
} else {
|
|
1856
|
+
if (titleEl) titleEl.textContent = '✅ 导入完成';
|
|
1857
|
+
if (statusEl) statusEl.textContent = '🎉 全部导入完成!';
|
|
1858
|
+
showFinalStats();
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function showFinalStats() {
|
|
1863
|
+
var failTxt = totalFailed > 0 ? ' · 失败: ' + totalFailed : '';
|
|
1864
|
+
var relTxt = totalRelationsCreated > 0 ? ' · 关系: ' + totalRelationsCreated + ' 条' : '';
|
|
1865
|
+
if (detailEl) detailEl.textContent = '共保存 ' + totalSaved + ' 条记忆' + failTxt + relTxt + ' · 跳过已有: ' + totalSkipped;
|
|
1866
|
+
if (cancelBtn) { cancelBtn.textContent = '关闭'; cancelBtn.onclick = function() { closeAutoImport(); }; }
|
|
1867
|
+
|
|
1868
|
+
// refresh memory list
|
|
1869
|
+
memoryLoaded = false;
|
|
1870
|
+
setTimeout(function() {
|
|
1871
|
+
closeAutoImport();
|
|
1872
|
+
loadMemoryPage();
|
|
1873
|
+
}, 2000);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
loadAndSaveBatch();
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function cancelAutoImport() {
|
|
1880
|
+
_autoImportCancelled = true;
|
|
1881
|
+
var cancelBtn = document.getElementById('memAutoImportCancelBtn');
|
|
1882
|
+
if (cancelBtn) { cancelBtn.disabled = true; cancelBtn.textContent = '取消中...'; }
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function closeAutoImport() {
|
|
1886
|
+
var overlay = document.getElementById('memAutoImportOverlay');
|
|
1887
|
+
if (overlay) overlay.style.display = 'none';
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
`;
|
|
1891
|
+
}
|
|
1892
|
+
//# sourceMappingURL=template-pages.js.map
|