dsh-memory-engine 0.7.0-beta.2b → 0.7.0-beta.6b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -34
- package/lib/auto-import.js +227 -0
- package/lib/auto-log.js +30 -0
- package/lib/auto-memory.js +156 -0
- package/lib/auto-sources.js +106 -0
- package/lib/automation-config.js +66 -0
- package/lib/automation-state.js +43 -0
- package/lib/classify.js +11 -0
- package/lib/client.js +476 -39
- package/lib/client.src.js +326 -17
- package/lib/config.js +38 -5
- package/lib/dedupe.js +2 -2
- package/lib/file-import.js +4 -3
- package/lib/import.js +8 -5
- package/lib/index.js +28 -6
- package/lib/map.js +10 -3
- package/lib/noise.js +20 -0
- package/lib/recall.js +67 -28
- package/lib/render/canvas.js +150 -22
- package/lib/render/layout.mjs +43 -0
- package/lib/render/render.mjs +24 -3
- package/lib/routes.js +93 -8
- package/lib/session-text.js +44 -0
- package/lib/store.js +49 -4
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -22,14 +22,18 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
if (!ctx) throw new Error('Canvas 2D 不可用');
|
|
23
23
|
const colors = { '人格':[255,215,106], '印象':[143,216,255], '知识':[126,231,178], '日记':[190,160,255], '对话':[255,160,140], '情绪':[255,90,90] };
|
|
24
24
|
const fallback = [145,205,255];
|
|
25
|
-
let nodes = [], links = [], hubs = [];
|
|
25
|
+
let nodes = [], links = [], hubs = [], linkTwinkle = [], visibleLinks = [];
|
|
26
26
|
const byId = new Map(), adjacency = new Map(), sprites = new Map();
|
|
27
27
|
const pulses = [], firing = new Map(), selections = new Set();
|
|
28
|
+
// 默认不画连线(数量太大会卡),只画选中光点周围的线
|
|
29
|
+
let selectedId=null, selectedNeighbours=new Set();
|
|
28
30
|
if (typeof options.onSelect === 'function') selections.add(options.onSelect);
|
|
29
31
|
let seed = 987654321;
|
|
30
32
|
const random = () => ((seed = seed * 16807 % 2147483647) - 1) / 2147483646;
|
|
31
33
|
let width=0,height=0,dpr=1,rotX=0.3,rotY=0.15,zoom=data.nodes.length<50?1.6:1,autoSpin=true;
|
|
32
|
-
let
|
|
34
|
+
let spinSpeed=data.spinSpeed??0.35,resumeAt=0;
|
|
35
|
+
const frameInterval=Number.isFinite(options.frameInterval)?Math.max(0,options.frameInterval):0;
|
|
36
|
+
let drag=null,nextFire=0,lastTime=0,lastDrawAt=0,rafId=null,running=false,destroyed=false,generation=0,observer=null;
|
|
33
37
|
|
|
34
38
|
function glowSprite(rgb) {
|
|
35
39
|
const off = document.createElement('canvas');
|
|
@@ -43,11 +47,26 @@ function glowSprite(rgb) {
|
|
|
43
47
|
return off;
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
// 字符串 → 32 位无符号哈希。给连线的闪烁参数做种子:
|
|
51
|
+
// 用 id 而不是 Math.random,保证同一条线在每次打开时节奏一致(不会每次刷新都跳),
|
|
52
|
+
// 但不同连线之间互不同步。
|
|
53
|
+
function hashString(text) {
|
|
54
|
+
let hash = 2166136261;
|
|
55
|
+
for (let i = 0; i < text.length; i++) {
|
|
56
|
+
hash ^= text.charCodeAt(i);
|
|
57
|
+
hash = Math.imul(hash, 16777619);
|
|
58
|
+
}
|
|
59
|
+
return hash >>> 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
function setData(nextData) {
|
|
47
63
|
if (destroyed) return;
|
|
64
|
+
spinSpeed=nextData.spinSpeed??0.35;
|
|
48
65
|
nodes = nextData.nodes.map(node => ({ ...node, pos: { ...node.pos } }));
|
|
49
66
|
links = nextData.links.map(link => [...link]);
|
|
50
67
|
byId.clear(); adjacency.clear(); sprites.clear(); pulses.length = 0; firing.clear();
|
|
68
|
+
selectedId=typeof options.selectedId==='string'?options.selectedId:null;
|
|
69
|
+
selectedNeighbours=new Set(selectedId?(adjacency.get(selectedId)||[]):[]);
|
|
51
70
|
seed = 987654321; nextFire = 0;
|
|
52
71
|
for (const node of nextData.nodes) byId.set(node.id, node);
|
|
53
72
|
for (const node of nodes) adjacency.set(node.id, []);
|
|
@@ -55,6 +74,41 @@ function glowSprite(rgb) {
|
|
|
55
74
|
if (adjacency.has(left) && adjacency.has(right)) { adjacency.get(left).push(right); adjacency.get(right).push(left); }
|
|
56
75
|
}
|
|
57
76
|
hubs = nodes.filter(star => star.degree > 0).sort((left,right) => right.degree-left.degree).slice(0,6);
|
|
77
|
+
// 每条连线一份自己的闪烁参数:相位和频率都不一样,整体才不会"齐刷刷"地一起亮。
|
|
78
|
+
// 用连线两端 id 的哈希做种子,保证同一条线每次打开图的节奏一致(不会跳变),
|
|
79
|
+
// 但线与线之间互不同步。
|
|
80
|
+
linkTwinkle = links.map(([left, right]) => {
|
|
81
|
+
const h = hashString(left + '|' + right);
|
|
82
|
+
return {
|
|
83
|
+
// 频率调低:周期从 4~18 秒放慢到 18~55 秒,变成"慢慢呼吸"而不是"闪"
|
|
84
|
+
freq: 0.000114 + (h % 1000) / 1000 * 0.000235,
|
|
85
|
+
phase: (Math.floor(h / 1000) % 6283) / 1000, // 0 ~ 6.28,彼此错开
|
|
86
|
+
amp: 0.05 + (Math.floor(h / 7e6) % 100) / 100 * 0.12,
|
|
87
|
+
// 弯曲:一半往左弯一半往右弯,弯度 8%~24%。
|
|
88
|
+
// 之前所有线都朝同一侧弯,视觉上像漩涡,边界区域就显空。
|
|
89
|
+
bow: (h % 2 === 0 ? 1 : -1) * (0.08 + (Math.floor(h / 13) % 100) / 100 * 0.16),
|
|
90
|
+
// 端点偏移:同一个节点被很多线连时,让它们从不同角度进来,散成扇面而不是叠成一条
|
|
91
|
+
jitterStart: ((Math.floor(h / 31) % 100) / 100 - 0.5) * 1.6,
|
|
92
|
+
jitterEnd: ((Math.floor(h / 977) % 100) / 100 - 0.5) * 1.6,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
// 连线数量会随记忆增长爆炸(332 条时 383 根,479 条时 1738 根)。
|
|
96
|
+
// 每帧全画会卡,所以按"两端留存"排序,最多画 LINK_DRAW_CAP 根最亮的,
|
|
97
|
+
// 其余的不进这一帧(数据没丢,只是不画,缩放/点击仍按全量算)。
|
|
98
|
+
const LINK_DRAW_CAP = 1200;
|
|
99
|
+
if (links.length <= LINK_DRAW_CAP) {
|
|
100
|
+
visibleLinks = links.map((_, index) => index);
|
|
101
|
+
} else {
|
|
102
|
+
const byIdLocal = new Map(nodes.map(node => [node.id, node]));
|
|
103
|
+
const strength = index => {
|
|
104
|
+
const [left, right] = links[index];
|
|
105
|
+
const a = byIdLocal.get(left), b = byIdLocal.get(right);
|
|
106
|
+
return Math.min(a?.retention ?? 0, b?.retention ?? 0) + Math.min(a?.degree ?? 0, b?.degree ?? 0) * 0.002;
|
|
107
|
+
};
|
|
108
|
+
visibleLinks = links.map((_, index) => index)
|
|
109
|
+
.sort((x, y) => strength(y) - strength(x))
|
|
110
|
+
.slice(0, LINK_DRAW_CAP);
|
|
111
|
+
}
|
|
58
112
|
for (const star of nodes) {
|
|
59
113
|
star._rgb = colors[star.kind] || fallback;
|
|
60
114
|
const count = Math.min(9, 4 + Math.round(Math.sqrt(star.degree) * 1.2));
|
|
@@ -90,9 +144,21 @@ function project(pos) {
|
|
|
90
144
|
}
|
|
91
145
|
function fire(id,now) {
|
|
92
146
|
firing.set(id,1);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
147
|
+
const neighbours=adjacency.get(id)||[];
|
|
148
|
+
// 选中某个光点时,把它的关联线都点亮几条脉冲(最多 24 条,避免又糊成一片);
|
|
149
|
+
// 未选中时的随机心跳只挑 2~4 条,避免整片区域同时亮。
|
|
150
|
+
const selected = id===selectedId;
|
|
151
|
+
const limit = selected ? 24 : 2+Math.floor(Math.random()*3);
|
|
152
|
+
const count=Math.min(neighbours.length,limit);
|
|
153
|
+
const picked=new Set();
|
|
154
|
+
let guard=0;
|
|
155
|
+
while (picked.size<count && neighbours.length && guard++<400) {
|
|
156
|
+
picked.add(neighbours[Math.floor(Math.random()*neighbours.length)]);
|
|
157
|
+
}
|
|
158
|
+
for (const other of picked) {
|
|
159
|
+
// 邻居亮度也压低:原来 0.65 会让整片区域同时亮起来
|
|
160
|
+
firing.set(other,Math.max(selected?0.4:0.26,firing.get(other)||0));
|
|
161
|
+
pulses.push({from:id,to:other,born:now,life:700+Math.random()*2400});
|
|
96
162
|
}
|
|
97
163
|
}
|
|
98
164
|
|
|
@@ -107,11 +173,14 @@ function fire(id,now) {
|
|
|
107
173
|
if (score<bestScore) {bestScore=score;best=star;}
|
|
108
174
|
}
|
|
109
175
|
if (!best) return;
|
|
176
|
+
selectedId = best.id;
|
|
177
|
+
selectedNeighbours = new Set(adjacency.get(best.id) || []);
|
|
110
178
|
fire(best.id,performance.now());
|
|
111
179
|
for (const callback of selections) callback(byId.get(best.id));
|
|
112
180
|
}
|
|
113
181
|
|
|
114
182
|
function onPointerDown(event) {
|
|
183
|
+
resumeAt=Infinity;
|
|
115
184
|
autoSpin=false; drag={x:event.clientX,y:event.clientY,moved:0,pointerId:event.pointerId};
|
|
116
185
|
canvas.setPointerCapture(event.pointerId);
|
|
117
186
|
}
|
|
@@ -122,6 +191,7 @@ function fire(id,now) {
|
|
|
122
191
|
rotY+=dx*0.006; rotX=Math.max(-1.5,Math.min(1.5,rotX+dy*0.006));
|
|
123
192
|
}
|
|
124
193
|
function releaseDrag() {
|
|
194
|
+
if (drag && running) resumeAt=performance.now()+2000;
|
|
125
195
|
if (drag && canvas.hasPointerCapture(drag.pointerId)) canvas.releasePointerCapture(drag.pointerId);
|
|
126
196
|
drag=null;
|
|
127
197
|
}
|
|
@@ -140,13 +210,27 @@ function fire(id,now) {
|
|
|
140
210
|
|
|
141
211
|
function frame(now) {
|
|
142
212
|
const delta=Math.min(50,now-lastTime||16.7); lastTime=now;
|
|
143
|
-
if (autoSpin)
|
|
144
|
-
if (
|
|
213
|
+
if (!autoSpin && !drag && now>=resumeAt) autoSpin=true;
|
|
214
|
+
if (autoSpin) rotY+=0.0011*spinSpeed*3*delta/16.7;
|
|
215
|
+
if (now>nextFire) {
|
|
216
|
+
// 默认不画连线,所以也不撒"沿线的脉冲"——否则会出现飘来飘去的小点,找不到来源。
|
|
217
|
+
// 选中光点后,脉冲只在它的关联线上跑。
|
|
218
|
+
if (selectedId) {
|
|
219
|
+
fire(selectedId,now);
|
|
220
|
+
} else {
|
|
221
|
+
// 没选中时只让枢纽光点自己亮一下,不动线
|
|
222
|
+
if (hubs.length) {
|
|
223
|
+
const hub=hubs[Math.floor(Math.random()*hubs.length)];
|
|
224
|
+
firing.set(hub.id,1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
nextFire=now+1400+Math.random()*3200;
|
|
228
|
+
}
|
|
145
229
|
ctx.globalCompositeOperation='source-over'; ctx.fillStyle='#03060c'; ctx.fillRect(0,0,width,height);
|
|
146
230
|
ctx.strokeStyle='rgba(40,90,140,0.055)';ctx.lineWidth=1;
|
|
147
231
|
for (let index=0;index<20;index++) {
|
|
148
232
|
const y=index/20*height;
|
|
149
|
-
ctx.beginPath();ctx.moveTo(0,y);ctx.bezierCurveTo(width*.3,y+Math.sin(now*.
|
|
233
|
+
ctx.beginPath();ctx.moveTo(0,y);ctx.bezierCurveTo(width*.3,y+Math.sin(now*.00007+index)*30,width*.7,y-Math.cos(now*.00006+index)*30,width,y);ctx.stroke();
|
|
150
234
|
}
|
|
151
235
|
const points=[];
|
|
152
236
|
for (const star of nodes) {
|
|
@@ -154,19 +238,39 @@ function frame(now) {
|
|
|
154
238
|
if (!p || p.x< -100 || p.x>width+100 || p.y< -100 || p.y>height+100) {star._screen=null;continue;}
|
|
155
239
|
points.push({star,p});
|
|
156
240
|
}
|
|
157
|
-
points.sort((left,right)=>right.p.depth-left.p.depth);
|
|
241
|
+
points.sort((left,right)=>left.star.isCore?1:right.star.isCore?-1:right.p.depth-left.p.depth);
|
|
158
242
|
const screen=new Map(points.map(point=>[point.star.id,point]));
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
243
|
+
// 连线只在"选中某个光点"时画,且只画和它相连的那些。
|
|
244
|
+
// 全量 1738 根每帧画会卡;默认干干净净,点谁看谁的关联。
|
|
245
|
+
const linkPaths=[];
|
|
246
|
+
if (selectedId) {
|
|
247
|
+
for (const [left,right] of links) {
|
|
248
|
+
if (left!==selectedId && right!==selectedId) continue;
|
|
249
|
+
const first=screen.get(left),second=screen.get(right);
|
|
250
|
+
if (!first||!second) continue;
|
|
251
|
+
const target = left===selectedId ? second : first;
|
|
252
|
+
const ret = Math.min(first.star.retention, second.star.retention);
|
|
253
|
+
const alpha = Math.min(.9, .22 + ret*.35);
|
|
254
|
+
const dx=second.p.x-first.p.x, dy=second.p.y-first.p.y;
|
|
255
|
+
const len=Math.hypot(dx,dy)||1;
|
|
256
|
+
const nx=-dy/len, ny=dx/len;
|
|
257
|
+
// 朝选中点那一侧弯,形成"从中心散开"的观感
|
|
258
|
+
const bow = (target===second ? 1 : -1) * .16;
|
|
259
|
+
const mx=(first.p.x+second.p.x)/2+nx*len*bow*.5;
|
|
260
|
+
const my=(first.p.y+second.p.y)/2+ny*len*bow*.5;
|
|
261
|
+
linkPaths.push([first.p.x,first.p.y,mx,my,second.p.x,second.p.y,alpha]);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (linkPaths.length) {
|
|
265
|
+
ctx.strokeStyle='rgba(110,190,255,0.30)';
|
|
266
|
+
ctx.lineWidth=1;
|
|
267
|
+
ctx.beginPath();
|
|
268
|
+
for (const p of linkPaths) { ctx.moveTo(p[0],p[1]); ctx.quadraticCurveTo(p[2],p[3],p[4],p[5]); }
|
|
269
|
+
ctx.stroke();
|
|
270
|
+
// 更亮的一层,让关联线看得清
|
|
271
|
+
ctx.strokeStyle='rgba(160,220,255,0.22)';
|
|
272
|
+
ctx.lineWidth=2;
|
|
273
|
+
ctx.stroke();
|
|
170
274
|
}
|
|
171
275
|
for (let index=pulses.length-1;index>=0;index--) {
|
|
172
276
|
const pulse=pulses[index],fraction=(now-pulse.born)/pulse.life;
|
|
@@ -182,8 +286,25 @@ function frame(now) {
|
|
|
182
286
|
for (const {star,p} of points) {
|
|
183
287
|
const act=firing.get(star.id)||0,ret=Math.max(.05,Math.min(1.2,star.retention));
|
|
184
288
|
const intensity=Math.min(1,.22+ret*.72)*(1+act*1.1);
|
|
185
|
-
|
|
289
|
+
// 每颗星自己的呼吸频率和相位:从 id 哈希来,避免全图同一个节拍
|
|
290
|
+
if (star._pulse===undefined) {
|
|
291
|
+
const h=hashString(star.id);
|
|
292
|
+
star._pulse={freq:0.00018+(h%1000)/1000*0.00045,phase:(Math.floor(h/1000)%6283)/1000,amp:0.035+(Math.floor(h/7e6)%100)/100*0.045};
|
|
293
|
+
}
|
|
294
|
+
const pulse=star._pulse;
|
|
295
|
+
const breathe=Math.sin(now*pulse.freq+pulse.phase)*pulse.amp;
|
|
296
|
+
const radius=Math.max(2,star.radius*p.scale*.55)*(1+breathe)*(1+act*.5);
|
|
186
297
|
const rgb=star._rgb.join(',');
|
|
298
|
+
if (star.isCore) {
|
|
299
|
+
const halo=ctx.createRadialGradient(p.x,p.y,radius*.75,p.x,p.y,radius*2.2);
|
|
300
|
+
halo.addColorStop(0,'rgba('+rgb+',0.12)');
|
|
301
|
+
halo.addColorStop(.65,'rgba('+rgb+',0.04)');
|
|
302
|
+
halo.addColorStop(1,'rgba('+rgb+',0)');
|
|
303
|
+
ctx.fillStyle=halo;ctx.beginPath();ctx.arc(p.x,p.y,radius*2.2,0,Math.PI*2);ctx.fill();
|
|
304
|
+
ctx.strokeStyle='rgba('+rgb+',0.38)';ctx.lineWidth=.7;ctx.setLineDash([3,5]);ctx.lineDashOffset=-now*.008;
|
|
305
|
+
ctx.beginPath();ctx.arc(p.x,p.y,radius*1.6,0,Math.PI*2);ctx.stroke();
|
|
306
|
+
ctx.setLineDash([]);ctx.lineDashOffset=0;
|
|
307
|
+
}
|
|
187
308
|
ctx.strokeStyle='rgba('+rgb+','+Math.min(1,.3*intensity)+')';
|
|
188
309
|
if (radius>2.5) for (const branch of star._dendrites) {
|
|
189
310
|
const length=radius*2.2*branch.length,dx=Math.cos(branch.angle),dy=Math.sin(branch.angle);
|
|
@@ -218,7 +339,7 @@ function frame(now) {
|
|
|
218
339
|
}
|
|
219
340
|
function start() {
|
|
220
341
|
if (running || destroyed) return;
|
|
221
|
-
running=true; lastTime=0; nextFire=0;
|
|
342
|
+
running=true; lastTime=0; nextFire=0; autoSpin=true; resumeAt=0;
|
|
222
343
|
const currentGeneration = ++generation;
|
|
223
344
|
canvas.addEventListener('pointerdown',onPointerDown);
|
|
224
345
|
canvas.addEventListener('pointermove',onPointerMove);
|
|
@@ -234,6 +355,13 @@ function frame(now) {
|
|
|
234
355
|
function tick(now) {
|
|
235
356
|
if (!running || destroyed || currentGeneration !== generation) return;
|
|
236
357
|
rafId=null;
|
|
358
|
+
// 帧率上限靠 options.frameInterval 控制(默认 0 = 不限)。
|
|
359
|
+
// 界面里按 40fps 传;测试里不传,保持逐帧确定性。
|
|
360
|
+
if (frameInterval > 0 && now - lastDrawAt < frameInterval) {
|
|
361
|
+
rafId=window.requestAnimationFrame(tick);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
lastDrawAt = now;
|
|
237
365
|
frame(now);
|
|
238
366
|
if (running && !destroyed && currentGeneration === generation) rafId=window.requestAnimationFrame(tick);
|
|
239
367
|
}
|
|
@@ -277,17 +405,21 @@ function frame(now) {
|
|
|
277
405
|
|
|
278
406
|
var defaultSettings = { hideButton: false, showGuideOnEmpty: true };
|
|
279
407
|
var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid #6999b8", background: "#102236", color: "#e2f4ff", font: "inherit", cursor: "pointer" };
|
|
408
|
+
var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
|
|
409
|
+
var kinds = ["知识", "印象", "对话", "事件", "日记"];
|
|
280
410
|
|
|
281
|
-
function
|
|
282
|
-
return fetch(url, { cache: "no-store",
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
411
|
+
function requestApi(url, options) {
|
|
412
|
+
return fetch(url, Object.assign({ cache: "no-store" }, options)).then(function (response) {
|
|
413
|
+
return response.json().then(function (result) {
|
|
414
|
+
if (!response.ok || !result || result.ok !== true) throw new Error(result && result.error || "HTTP " + response.status);
|
|
415
|
+
if (!result.data || typeof result.data !== "object") throw new Error("接口返回格式无效");
|
|
416
|
+
return result.data;
|
|
417
|
+
});
|
|
288
418
|
});
|
|
289
419
|
}
|
|
290
420
|
|
|
421
|
+
function readApi(url, signal) { return requestApi(url, { signal: signal }); }
|
|
422
|
+
|
|
291
423
|
function loadSettings() {
|
|
292
424
|
var controller = new AbortController();
|
|
293
425
|
var timer = setTimeout(function () { controller.abort(); }, 5000);
|
|
@@ -367,6 +499,8 @@ function frame(now) {
|
|
|
367
499
|
function MemoryDetails(props) {
|
|
368
500
|
var node = props.node;
|
|
369
501
|
var text = typeof node.text === "string" ? node.text : "";
|
|
502
|
+
if (node.isCore) text = Array.from(text).slice(0, 4000).join("");
|
|
503
|
+
var defaultCopyLabel = node.isCore ? "复制全文" : "复制正文";
|
|
370
504
|
var title = String(node.title || "").trim() || Array.from(text).slice(0, 20).join("");
|
|
371
505
|
var date = new Date(node.ts);
|
|
372
506
|
var day = Number.isFinite(date.getTime()) ? date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") : "未知";
|
|
@@ -379,7 +513,7 @@ function frame(now) {
|
|
|
379
513
|
];
|
|
380
514
|
var emotion = Array.isArray(node.emotion) ? node.emotion.filter(function (item) { return item && item.tag; }).map(function (item) { return item.tag; }).join(" / ") : "";
|
|
381
515
|
if (emotion) rows.push(["情绪", emotion]);
|
|
382
|
-
var copiedState = React.useState(
|
|
516
|
+
var copiedState = React.useState(defaultCopyLabel);
|
|
383
517
|
var copyLabel = copiedState[0];
|
|
384
518
|
var setCopyLabel = copiedState[1];
|
|
385
519
|
var timer = React.useRef(null);
|
|
@@ -389,10 +523,10 @@ function frame(now) {
|
|
|
389
523
|
copyVersion.current++;
|
|
390
524
|
clearTimeout(timer.current);
|
|
391
525
|
timer.current = null;
|
|
392
|
-
setCopyLabel(
|
|
526
|
+
setCopyLabel(defaultCopyLabel);
|
|
393
527
|
if (body.current) body.current.scrollTop = 0;
|
|
394
528
|
return function () { copyVersion.current++; clearTimeout(timer.current); timer.current = null; };
|
|
395
|
-
}, [node.id, text]);
|
|
529
|
+
}, [node.id, text, defaultCopyLabel]);
|
|
396
530
|
function copyBody() {
|
|
397
531
|
var version = ++copyVersion.current;
|
|
398
532
|
clearTimeout(timer.current);
|
|
@@ -400,7 +534,7 @@ function frame(now) {
|
|
|
400
534
|
if (version !== copyVersion.current) return;
|
|
401
535
|
setCopyLabel("已复制 ✓");
|
|
402
536
|
timer.current = setTimeout(function () {
|
|
403
|
-
if (version === copyVersion.current) setCopyLabel(
|
|
537
|
+
if (version === copyVersion.current) setCopyLabel(defaultCopyLabel);
|
|
404
538
|
timer.current = null;
|
|
405
539
|
}, 1800);
|
|
406
540
|
}, function () {
|
|
@@ -414,13 +548,13 @@ function frame(now) {
|
|
|
414
548
|
React.createElement("span", { "data-memory-engine-detail-kind": "true", style: { color: "#a4e6ff", background: "#15344b", borderRadius: "6px", padding: "2px 8px", overflowWrap: "anywhere" } }, node.kind || "未分类"),
|
|
415
549
|
React.createElement("button", { type: "button", "aria-label": "关闭记忆详情", onClick: props.onClose, style: { border: 0, padding: "2px 6px", background: "transparent", color: "#a4bad0", fontSize: "22px", cursor: "pointer" } }, "×")),
|
|
416
550
|
React.createElement("h2", { "data-memory-engine-detail-title": "true", style: { margin: "10px 0", fontSize: "18px", color: "#b7e9ff", overflowWrap: "anywhere", whiteSpace: "normal", maxHeight: "18vh", overflowY: "auto", flexShrink: 0 } }, title),
|
|
417
|
-
React.createElement("dl", { style: { display: "grid", gridTemplateColumns: "64px minmax(0,1fr)", gap: "5px 10px", margin: "0 0 12px", fontSize: "12px", color: "#9fb7ce", maxHeight: "25vh", overflowY: "auto", flexShrink: 0 } },
|
|
551
|
+
node.isCore ? null : React.createElement("dl", { style: { display: "grid", gridTemplateColumns: "64px minmax(0,1fr)", gap: "5px 10px", margin: "0 0 12px", fontSize: "12px", color: "#9fb7ce", maxHeight: "25vh", overflowY: "auto", flexShrink: 0 } },
|
|
418
552
|
rows.map(function (row) { return React.createElement(React.Fragment, { key: row[0] },
|
|
419
553
|
React.createElement("dt", { style: { margin: 0 } }, row[0]),
|
|
420
554
|
React.createElement("dd", { style: { margin: 0, overflowWrap: "anywhere" } }, row[1])); })),
|
|
421
555
|
React.createElement("div", { ref: body, "data-memory-engine-detail-text": "true", style: { minHeight: 0, flex: "1 1 auto", overflowY: "auto", whiteSpace: "pre-wrap", overflowWrap: "anywhere", borderTop: "1px solid #3976a055", borderBottom: "1px solid #3976a055", padding: "12px 0" } }, text),
|
|
422
556
|
React.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px", marginTop: "12px", flexShrink: 0 } },
|
|
423
|
-
React.createElement("button", { type: "button", "data-memory-engine-highlight": "true", style: actionStyle, onClick: props.onHighlight }, "在图里高亮它"),
|
|
557
|
+
node.isCore ? null : React.createElement("button", { type: "button", "data-memory-engine-highlight": "true", style: actionStyle, onClick: props.onHighlight }, "在图里高亮它"),
|
|
424
558
|
React.createElement("button", { type: "button", "data-memory-engine-copy-body": "true", style: actionStyle, onClick: copyBody }, copyLabel)));
|
|
425
559
|
}
|
|
426
560
|
|
|
@@ -442,7 +576,7 @@ function frame(now) {
|
|
|
442
576
|
setError("");
|
|
443
577
|
readApi("/memory-engine/api/map", controller.signal).then(function (nextData) {
|
|
444
578
|
if (!active) return;
|
|
445
|
-
renderer = createRenderer(canvas.current, nextData);
|
|
579
|
+
renderer = createRenderer(canvas.current, nextData, { frameInterval: 25 });
|
|
446
580
|
props.rendererRef.current = renderer;
|
|
447
581
|
renderer.onSelect(function (node) { props.selectionVersion.current++; props.onSelect(node); });
|
|
448
582
|
renderer.start();
|
|
@@ -482,12 +616,303 @@ function frame(now) {
|
|
|
482
616
|
React.createElement("span", { style: { width: "52px", height: "5px", borderRadius: "5px", background: "#335066", overflow: "hidden" } },
|
|
483
617
|
React.createElement("i", { style: { display: "block", height: "100%", background: "#6bc9fa", width: Math.min(100, Math.max(0, item.retention * 100)) + "%" } })));
|
|
484
618
|
})) : null,
|
|
485
|
-
React.createElement("div", { style: { position: "absolute", right: "18px", bottom: "18px", pointerEvents: "none", color: "#7b99ad", font: "13px system-ui,sans-serif" } }, "拖拽旋转 · 滚轮缩放 ·
|
|
619
|
+
React.createElement("div", { style: { position: "absolute", right: "18px", bottom: "18px", pointerEvents: "none", color: "#7b99ad", font: "13px system-ui,sans-serif" } }, "拖拽旋转 · 滚轮缩放 · 点光点看关联"),
|
|
486
620
|
error ? React.createElement("div", { role: "alert", style: { position: "absolute", inset: "40% 20% auto", textAlign: "center", color: "#d8e9f8" } },
|
|
487
621
|
React.createElement("p", null, "图谱加载失败:" + error),
|
|
488
622
|
React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null);
|
|
489
623
|
}
|
|
490
624
|
|
|
625
|
+
function ConfirmationDialog(props) {
|
|
626
|
+
var valueState = React.useState("");
|
|
627
|
+
var value = valueState[0], setValue = valueState[1];
|
|
628
|
+
var errorState = React.useState("");
|
|
629
|
+
var error = errorState[0], setError = errorState[1];
|
|
630
|
+
var form = React.useRef(null);
|
|
631
|
+
React.useEffect(function () {
|
|
632
|
+
var active = document.activeElement;
|
|
633
|
+
var target = form.current && form.current.querySelector("input,button");
|
|
634
|
+
if (target) target.focus();
|
|
635
|
+
return function () { if (active && active.isConnected) active.focus(); };
|
|
636
|
+
}, []);
|
|
637
|
+
function onKey(event) {
|
|
638
|
+
if (event.key === "Escape") {
|
|
639
|
+
event.preventDefault(); event.stopPropagation();
|
|
640
|
+
if (!props.busy) props.onClose();
|
|
641
|
+
}
|
|
642
|
+
if (event.key === "Tab" && form.current) {
|
|
643
|
+
var controls = Array.from(form.current.querySelectorAll("input,button")).filter(function (control) { return !control.disabled; });
|
|
644
|
+
if (!controls.length) { event.preventDefault(); return; }
|
|
645
|
+
var index = controls.indexOf(document.activeElement);
|
|
646
|
+
if (event.shiftKey && index <= 0) { event.preventDefault(); controls[controls.length - 1].focus(); }
|
|
647
|
+
else if (!event.shiftKey && (index === -1 || index === controls.length - 1)) { event.preventDefault(); controls[0].focus(); }
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return React.createElement("div", { "data-memory-engine-confirm": "true", onKeyDown: onKey,
|
|
651
|
+
style: { position: "fixed", inset: 0, zIndex: 3, display: "grid", placeItems: "center", padding: "24px", background: "#03060ccc" } },
|
|
652
|
+
React.createElement("form", { ref: form, role: "dialog", "aria-modal": "true", "aria-label": props.title,
|
|
653
|
+
onSubmit: function (event) {
|
|
654
|
+
event.preventDefault();
|
|
655
|
+
if (props.busy) return;
|
|
656
|
+
if (props.requireDelete && value !== "DELETE") { setError("请输入大写 DELETE,其他内容不会执行清空。"); return; }
|
|
657
|
+
setError(""); props.onConfirm();
|
|
658
|
+
},
|
|
659
|
+
style: { width: "min(460px,100%)", padding: "24px", border: "1px solid #8b505c", borderRadius: "16px", background: "#101a29", color: "#e2f4ff", boxShadow: "0 16px 80px #0008" } },
|
|
660
|
+
React.createElement("h2", { style: { margin: "0 0 12px", fontSize: "20px" } }, props.title),
|
|
661
|
+
React.createElement("p", { style: { color: "#c2cedc" } }, props.message),
|
|
662
|
+
props.requireDelete ? React.createElement("label", null, "输入 DELETE 确认",
|
|
663
|
+
React.createElement("input", { "aria-label": "输入 DELETE 确认", autoComplete: "off", value: value, disabled: props.busy,
|
|
664
|
+
onChange: function (event) { setValue(event.target.value); setError(""); }, style: Object.assign({}, fieldStyle, { display: "block", width: "100%", marginTop: "8px" }) })) : null,
|
|
665
|
+
error || props.error ? React.createElement("p", { role: "alert", style: { color: "#ffb8b8" } }, error || props.error) : null,
|
|
666
|
+
React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: "10px", marginTop: "24px" } },
|
|
667
|
+
React.createElement("button", { type: "button", disabled: props.busy, onClick: props.onClose, style: actionStyle }, "取消"),
|
|
668
|
+
React.createElement("button", { type: "submit", disabled: props.busy, style: Object.assign({}, actionStyle, { borderColor: "#c86b7a", background: "#572a38" }) }, props.busy ? "正在处理…" : props.submitLabel))));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function MemoryRow(props) {
|
|
672
|
+
var item = props.item;
|
|
673
|
+
var expandedState = React.useState(false);
|
|
674
|
+
var expanded = expandedState[0], setExpanded = expandedState[1];
|
|
675
|
+
var detailState = React.useState(item);
|
|
676
|
+
var detail = detailState[0], setDetail = detailState[1];
|
|
677
|
+
var editState = React.useState(null);
|
|
678
|
+
var edit = editState[0], setEdit = editState[1];
|
|
679
|
+
var confirmState = React.useState(false);
|
|
680
|
+
var confirming = confirmState[0], setConfirming = confirmState[1];
|
|
681
|
+
var errorState = React.useState("");
|
|
682
|
+
var error = errorState[0], setError = errorState[1];
|
|
683
|
+
var loadingState = React.useState(false);
|
|
684
|
+
var loading = loadingState[0], setLoading = loadingState[1];
|
|
685
|
+
var busyState = React.useState(false);
|
|
686
|
+
var busy = busyState[0], setBusy = busyState[1];
|
|
687
|
+
var mutation = React.useRef(null);
|
|
688
|
+
React.useEffect(function () { return function () { if (mutation.current) mutation.current.abort(); }; }, []);
|
|
689
|
+
React.useEffect(function () {
|
|
690
|
+
if (!expanded) return;
|
|
691
|
+
var controller = new AbortController();
|
|
692
|
+
setLoading(true); setError("");
|
|
693
|
+
readApi("/memory-engine/api/item/" + encodeURIComponent(item.id), controller.signal).then(function (result) {
|
|
694
|
+
if (!controller.signal.aborted) setDetail(result);
|
|
695
|
+
}).catch(function (failure) {
|
|
696
|
+
if (!controller.signal.aborted) setError("读取详情失败:" + failure.message);
|
|
697
|
+
}).finally(function () { if (!controller.signal.aborted) setLoading(false); });
|
|
698
|
+
return function () { controller.abort(); };
|
|
699
|
+
}, [expanded, item.id]);
|
|
700
|
+
function mutate(method) {
|
|
701
|
+
if (mutation.current) return;
|
|
702
|
+
var controller = new AbortController();
|
|
703
|
+
mutation.current = controller;
|
|
704
|
+
setBusy(true); setError("");
|
|
705
|
+
var options = { method: method, signal: controller.signal };
|
|
706
|
+
if (method === "PUT") { options.headers = { "Content-Type": "application/json" }; options.body = JSON.stringify(edit); }
|
|
707
|
+
requestApi("/memory-engine/api/item/" + encodeURIComponent(item.id), options).then(function () {
|
|
708
|
+
if (controller.signal.aborted) return;
|
|
709
|
+
setEdit(null); setExpanded(false); setConfirming(false); props.onChanged();
|
|
710
|
+
}).catch(function (failure) {
|
|
711
|
+
if (!controller.signal.aborted) setError((method === "PUT" ? "保存失败:" : "删除失败:") + failure.message);
|
|
712
|
+
}).finally(function () {
|
|
713
|
+
if (!controller.signal.aborted) { mutation.current = null; setBusy(false); }
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
function change(field, value) { setEdit(function (previous) { var next = Object.assign({}, previous); next[field] = value; return next; }); }
|
|
717
|
+
var choices = kinds.includes(detail.kind) ? kinds : [detail.kind].concat(kinds);
|
|
718
|
+
return React.createElement("article", { "data-memory-engine-row": item.id,
|
|
719
|
+
style: { marginBottom: "12px", border: "1px solid " + (expanded ? "#4c85a4" : "#263f54"), borderRadius: "12px", background: "#0b1624", overflowWrap: "anywhere" } },
|
|
720
|
+
React.createElement("button", { type: "button", "data-memory-engine-expand": item.id, "aria-expanded": expanded, disabled: busy,
|
|
721
|
+
onClick: function () { setExpanded(!expanded); setEdit(null); setError(""); },
|
|
722
|
+
style: { width: "100%", display: "block", padding: "18px 20px", border: 0, borderRadius: "12px", background: "transparent", color: "inherit", textAlign: "left", font: "inherit", cursor: "pointer" } },
|
|
723
|
+
React.createElement("span", { style: { display: "flex", alignItems: "center", gap: "12px", flexWrap: "wrap" } },
|
|
724
|
+
React.createElement("span", { style: { color: "#a4e6ff", background: "#15344b", padding: "2px 8px", borderRadius: "6px", flexShrink: 0 } }, item.kind),
|
|
725
|
+
React.createElement("strong", { style: { flex: "1 1 180px", overflowWrap: "anywhere" } }, item.title),
|
|
726
|
+
React.createElement("span", { style: { color: "#8db0c7", fontSize: "12px", whiteSpace: "nowrap" } }, "留存 " + item.retention.toFixed(2) + " · " + item.degree + " 个连接")),
|
|
727
|
+
!expanded ? React.createElement("span", { style: { display: "block", color: "#9fb7ce", marginTop: "8px", overflowWrap: "anywhere" } }, item.preview) : null),
|
|
728
|
+
expanded ? React.createElement("div", { style: { padding: "0 20px 18px" } },
|
|
729
|
+
loading ? React.createElement("p", { role: "status" }, "正在读取全文…") : null,
|
|
730
|
+
edit ? React.createElement("form", { "data-memory-engine-editor": item.id, onSubmit: function (event) { event.preventDefault(); mutate("PUT"); } },
|
|
731
|
+
React.createElement("textarea", { "aria-label": "记忆正文", value: edit.text, disabled: busy,
|
|
732
|
+
onChange: function (event) { change("text", event.target.value); },
|
|
733
|
+
style: Object.assign({}, fieldStyle, { width: "100%", minHeight: "150px", maxHeight: "40vh", resize: "vertical", lineHeight: 1.7 }) }),
|
|
734
|
+
React.createElement("div", { style: { display: "flex", gap: "12px", flexWrap: "wrap", margin: "12px 0" } },
|
|
735
|
+
React.createElement("label", null, "分类 ", React.createElement("select", { "aria-label": "编辑分类", value: edit.kind, disabled: busy, style: fieldStyle,
|
|
736
|
+
onChange: function (event) { change("kind", event.target.value); } }, choices.map(function (value) { return React.createElement("option", { key: value, value: value }, value); }))),
|
|
737
|
+
React.createElement("label", null, "重要度 ", React.createElement("select", { "aria-label": "编辑重要度", value: edit.heat, disabled: busy, style: fieldStyle,
|
|
738
|
+
onChange: function (event) { change("heat", Number(event.target.value)); } },
|
|
739
|
+
[[1, "热(1)"], [2, "温(2)"], [3, "冷(3)"]].map(function (entry) { return React.createElement("option", { key: entry[0], value: entry[0] }, entry[1]); }))),
|
|
740
|
+
React.createElement("label", null, "相关人 ", React.createElement("input", { "aria-label": "编辑相关人", value: edit.who, disabled: busy,
|
|
741
|
+
onChange: function (event) { change("who", event.target.value); }, style: Object.assign({}, fieldStyle, { width: "140px" }) }))),
|
|
742
|
+
React.createElement("div", { style: { display: "flex", gap: "8px" } },
|
|
743
|
+
React.createElement("button", { type: "submit", disabled: busy, "data-memory-engine-save": "true", style: actionStyle }, busy ? "正在保存…" : "保存"),
|
|
744
|
+
React.createElement("button", { type: "button", disabled: busy, "data-memory-engine-cancel-edit": "true", style: actionStyle,
|
|
745
|
+
onClick: function () { setEdit(null); setExpanded(false); setError(""); } }, "取消")))
|
|
746
|
+
: React.createElement(React.Fragment, null,
|
|
747
|
+
React.createElement("div", { "data-memory-engine-full-text": "true", style: { whiteSpace: "pre-wrap", maxHeight: "40vh", overflowY: "auto", color: "#c2d3e3", marginBottom: "14px" } }, detail.text),
|
|
748
|
+
React.createElement("div", { style: { display: "flex", gap: "8px" } },
|
|
749
|
+
React.createElement("button", { type: "button", "data-memory-engine-edit": item.id, disabled: loading || busy, style: actionStyle,
|
|
750
|
+
onClick: function () { setError(""); setEdit({ text: detail.text, kind: detail.kind, heat: detail.heat, who: detail.who || "" }); } }, "编辑"),
|
|
751
|
+
React.createElement("button", { type: "button", "data-memory-engine-delete": item.id, disabled: loading || busy,
|
|
752
|
+
onClick: function () { setError(""); setConfirming(true); }, style: Object.assign({}, actionStyle, { color: "#ffb8b8", borderColor: "#8b505c" }) }, "删除"))),
|
|
753
|
+
error ? React.createElement("p", { role: "alert", style: { color: "#ffb8b8" } }, error) : null) : null,
|
|
754
|
+
confirming ? React.createElement(ConfirmationDialog, { title: "删除这条记忆", message: "确定删除这条记忆?不可撤销。操作前会保留备份。", submitLabel: "确定删除",
|
|
755
|
+
busy: busy, error: error, onClose: function () { setConfirming(false); }, onConfirm: function () { mutate("DELETE"); } }) : null);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function MemoryList(props) {
|
|
759
|
+
var searchState = React.useState("");
|
|
760
|
+
var search = searchState[0], setSearch = searchState[1];
|
|
761
|
+
var queryState = React.useState("");
|
|
762
|
+
var query = queryState[0], setQuery = queryState[1];
|
|
763
|
+
var kindState = React.useState("");
|
|
764
|
+
var kind = kindState[0], setKind = kindState[1];
|
|
765
|
+
var sortState = React.useState("recent");
|
|
766
|
+
var sort = sortState[0], setSort = sortState[1];
|
|
767
|
+
var offsetState = React.useState(0);
|
|
768
|
+
var offset = offsetState[0], setOffset = offsetState[1];
|
|
769
|
+
var pageState = React.useState({ items: [], total: 0, allTotal: 0 });
|
|
770
|
+
var page = pageState[0], setPage = pageState[1];
|
|
771
|
+
var loadingState = React.useState(true);
|
|
772
|
+
var loading = loadingState[0], setLoading = loadingState[1];
|
|
773
|
+
var errorState = React.useState("");
|
|
774
|
+
var error = errorState[0], setError = errorState[1];
|
|
775
|
+
var retryState = React.useState(0);
|
|
776
|
+
var retry = retryState[0], setRetry = retryState[1];
|
|
777
|
+
function refresh() {
|
|
778
|
+
setOffset(0); setRetry(function (value) { return value + 1; });
|
|
779
|
+
if (props.onChanged) props.onChanged();
|
|
780
|
+
}
|
|
781
|
+
React.useEffect(function () {
|
|
782
|
+
if (search === query) return;
|
|
783
|
+
var timer = setTimeout(function () { setOffset(0); setQuery(search); }, 250);
|
|
784
|
+
return function () { clearTimeout(timer); };
|
|
785
|
+
}, [search]);
|
|
786
|
+
React.useEffect(function () {
|
|
787
|
+
var controller = new AbortController();
|
|
788
|
+
var active = true;
|
|
789
|
+
setLoading(true); setError("");
|
|
790
|
+
var params = new URLSearchParams({ q: query, kind: kind, sort: sort, limit: "50", offset: String(offset) });
|
|
791
|
+
readApi("/memory-engine/api/list?" + params, controller.signal).then(function (result) {
|
|
792
|
+
if (!active) return;
|
|
793
|
+
setPage(function (previous) { return Object.assign({}, result, { items: offset === 0 ? result.items : previous.items.concat(result.items) }); });
|
|
794
|
+
}).catch(function (failure) {
|
|
795
|
+
if (active) setError("加载记忆失败:" + failure.message);
|
|
796
|
+
}).finally(function () { if (active) setLoading(false); });
|
|
797
|
+
return function () { active = false; controller.abort(); };
|
|
798
|
+
}, [query, kind, sort, offset, retry]);
|
|
799
|
+
return React.createElement("section", {
|
|
800
|
+
"aria-label": "记忆列表", "data-memory-engine-list": "true",
|
|
801
|
+
style: { position: "absolute", inset: "76px 0 0", display: "flex", flexDirection: "column", color: "#dbeaf8", font: "14px/1.6 system-ui,sans-serif" },
|
|
802
|
+
}, React.createElement("div", { style: { width: "min(980px,100%)", margin: "0 auto", padding: "0 24px 16px", display: "flex", gap: "12px", flexWrap: "wrap" } },
|
|
803
|
+
React.createElement("input", { type: "search", "aria-label": "搜索记忆", placeholder: "搜索正文或标题…", value: search,
|
|
804
|
+
onChange: function (event) { setSearch(event.target.value); }, style: Object.assign({}, fieldStyle, { flex: "1 1 220px" }) }),
|
|
805
|
+
React.createElement("select", { "aria-label": "分类筛选", value: kind, style: fieldStyle,
|
|
806
|
+
onChange: function (event) { setOffset(0); setKind(event.target.value); } },
|
|
807
|
+
[""].concat(kinds).map(function (value) { return React.createElement("option", { key: value, value: value }, value || "全部分类"); })),
|
|
808
|
+
React.createElement("select", { "aria-label": "记忆排序", value: sort, style: fieldStyle,
|
|
809
|
+
onChange: function (event) { setOffset(0); setSort(event.target.value); } },
|
|
810
|
+
[["recent", "最近"], ["oldest", "最老"], ["fading", "快忘了"], ["degree", "连接最多"]].map(function (entry) { return React.createElement("option", { key: entry[0], value: entry[0] }, entry[1]); }))),
|
|
811
|
+
React.createElement("div", { "data-memory-engine-list-scroll": "true", style: { flex: 1, minHeight: 0, overflowY: "auto", padding: "0 24px 24px" } },
|
|
812
|
+
React.createElement("div", { style: { maxWidth: "932px", margin: "0 auto" }, "aria-busy": loading },
|
|
813
|
+
error ? React.createElement("div", { role: "alert", style: { color: "#ffb8b8", padding: "16px" } }, error,
|
|
814
|
+
React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null,
|
|
815
|
+
page.items.map(function (item) {
|
|
816
|
+
return React.createElement(MemoryRow, { key: item.id, item: item, onChanged: refresh });
|
|
817
|
+
}),
|
|
818
|
+
!loading && !error && !page.items.length ? React.createElement("p", { role: "status", style: { padding: "24px", textAlign: "center", color: "#9fb7ce" } }, page.allTotal ? "没有匹配的记忆,试试其他关键词或分类。" : "还没有记忆,可以点右上角 ? 查看导入方法。") : null)),
|
|
819
|
+
React.createElement("div", { "data-memory-engine-list-total": "true", style: { padding: "14px", textAlign: "center", color: "#9fb7ce", flexShrink: 0, borderTop: "1px solid #263f54" } },
|
|
820
|
+
"共 " + page.total + " 条 · 已显示 " + page.items.length + " 条",
|
|
821
|
+
loading ? React.createElement("span", { role: "status" }, " · 正在加载…") : null,
|
|
822
|
+
page.items.length < page.total ? React.createElement("button", { type: "button", "data-memory-engine-more": "true", disabled: loading || search !== query || !!error,
|
|
823
|
+
onClick: function () { setOffset(page.items.length); }, style: Object.assign({}, actionStyle, { marginLeft: "12px", opacity: loading ? 0.5 : 1 }) }, "加载更多") : null));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function MemorySettings(props) {
|
|
827
|
+
var statsState = React.useState(null);
|
|
828
|
+
var stats = statsState[0], setStats = statsState[1];
|
|
829
|
+
var errorState = React.useState("");
|
|
830
|
+
var error = errorState[0], setError = errorState[1];
|
|
831
|
+
var noticeState = React.useState("");
|
|
832
|
+
var notice = noticeState[0], setNotice = noticeState[1];
|
|
833
|
+
var retryState = React.useState(0);
|
|
834
|
+
var retry = retryState[0], setRetry = retryState[1];
|
|
835
|
+
var busyState = React.useState(false);
|
|
836
|
+
var busy = busyState[0], setBusy = busyState[1];
|
|
837
|
+
var confirmState = React.useState(false);
|
|
838
|
+
var confirming = confirmState[0], setConfirming = confirmState[1];
|
|
839
|
+
var mutation = React.useRef(null);
|
|
840
|
+
var download = React.useRef(null);
|
|
841
|
+
var timer = React.useRef(null);
|
|
842
|
+
var active = React.useRef(true);
|
|
843
|
+
React.useEffect(function () {
|
|
844
|
+
active.current = true;
|
|
845
|
+
return function () {
|
|
846
|
+
active.current = false;
|
|
847
|
+
if (mutation.current) mutation.current.abort();
|
|
848
|
+
clearTimeout(timer.current);
|
|
849
|
+
if (download.current) URL.revokeObjectURL(download.current);
|
|
850
|
+
};
|
|
851
|
+
}, []);
|
|
852
|
+
React.useEffect(function () {
|
|
853
|
+
var controller = new AbortController();
|
|
854
|
+
setError("");
|
|
855
|
+
readApi("/memory-engine/api/stats", controller.signal).then(function (result) {
|
|
856
|
+
if (!controller.signal.aborted) setStats(result);
|
|
857
|
+
}).catch(function (failure) {
|
|
858
|
+
if (!controller.signal.aborted) setError("读取设置失败:" + failure.message);
|
|
859
|
+
});
|
|
860
|
+
return function () { controller.abort(); };
|
|
861
|
+
}, [retry]);
|
|
862
|
+
function manage(clearing) {
|
|
863
|
+
if (mutation.current) return;
|
|
864
|
+
var controller = new AbortController();
|
|
865
|
+
mutation.current = controller; setBusy(true); setError(""); setNotice("");
|
|
866
|
+
var options = { signal: controller.signal };
|
|
867
|
+
if (clearing) { options.method = "POST"; options.headers = { "Content-Type": "application/json" }; options.body = JSON.stringify({ confirm: "DELETE" }); }
|
|
868
|
+
requestApi("/memory-engine/api/" + (clearing ? "clear" : "export"), options).then(function (result) {
|
|
869
|
+
if (controller.signal.aborted) return;
|
|
870
|
+
if (clearing) { props.onCleared(); return; }
|
|
871
|
+
if (download.current) URL.revokeObjectURL(download.current);
|
|
872
|
+
clearTimeout(timer.current);
|
|
873
|
+
download.current = URL.createObjectURL(new Blob([JSON.stringify({ ok: true, data: result }, null, 2) + "\n"], { type: "application/json;charset=utf-8" }));
|
|
874
|
+
var anchor = document.createElement("a");
|
|
875
|
+
var date = new Date();
|
|
876
|
+
anchor.href = download.current;
|
|
877
|
+
anchor.download = "memories-" + date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") + ".json";
|
|
878
|
+
document.body.appendChild(anchor); anchor.click(); anchor.remove();
|
|
879
|
+
timer.current = setTimeout(function () { URL.revokeObjectURL(download.current); download.current = null; timer.current = null; }, 1000);
|
|
880
|
+
setNotice("已开始下载,共 " + result.total + " 条记忆。");
|
|
881
|
+
}).catch(function (failure) {
|
|
882
|
+
if (!controller.signal.aborted) setError((clearing ? "清空失败:" : "导出失败:") + failure.message);
|
|
883
|
+
}).finally(function () {
|
|
884
|
+
if (!controller.signal.aborted) { mutation.current = null; setBusy(false); }
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
var directory = stats ? stats.file.replace(/[\\/][^\\/]*$/, "") : "";
|
|
888
|
+
return React.createElement("section", { "aria-label": "记忆设置", "data-memory-engine-settings": "true",
|
|
889
|
+
style: { position: "absolute", inset: "76px 0 0", overflowY: "auto", padding: "0 24px 32px", color: "#dbeaf8", font: "14px/1.7 system-ui,sans-serif" } },
|
|
890
|
+
React.createElement("div", { style: { maxWidth: "900px", margin: "0 auto", padding: "28px", border: "1px solid #263f54", borderRadius: "14px", background: "#0b1624" } },
|
|
891
|
+
React.createElement("h2", { style: { margin: "0 0 20px", fontSize: "20px", color: "#b7e9ff" } }, "数据管理"),
|
|
892
|
+
stats ? React.createElement(React.Fragment, null,
|
|
893
|
+
React.createElement("div", { style: { display: "flex", alignItems: "center", flexWrap: "wrap", gap: "12px" } },
|
|
894
|
+
React.createElement("span", { style: { flex: "1 1 360px", overflowWrap: "anywhere" } }, "数据目录:", React.createElement("code", { "data-memory-engine-directory": "true" }, directory)),
|
|
895
|
+
React.createElement("button", { type: "button", "data-memory-engine-copy-path": "true", style: actionStyle,
|
|
896
|
+
onClick: function () {
|
|
897
|
+
setNotice(""); setError("");
|
|
898
|
+
copyText(directory).then(function () { if (active.current) setNotice("数据目录已复制 ✓"); }, function () { if (active.current) setError("复制路径失败,请手动复制上方目录。"); });
|
|
899
|
+
} }, "复制路径")),
|
|
900
|
+
React.createElement("p", { "data-memory-engine-storage-stats": "true", style: { color: "#9fb7ce", margin: "16px 0 24px" } }, "记忆条数:" + stats.total + " 文件大小:" + (stats.bytes / 1024).toFixed(1) + " KB"))
|
|
901
|
+
: React.createElement("p", { role: "status" }, error ? "暂时无法读取数据目录。" : "正在读取数据目录…"),
|
|
902
|
+
error ? React.createElement("div", { role: "alert", style: { color: "#ffb8b8", marginBottom: "16px" } }, error,
|
|
903
|
+
!stats ? React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试") : null) : null,
|
|
904
|
+
notice ? React.createElement("p", { role: "status", style: { color: "#9fe5c4" } }, notice) : null,
|
|
905
|
+
React.createElement("div", { style: { padding: "24px 0", borderTop: "1px solid #263f54", borderBottom: "1px solid #263f54" } },
|
|
906
|
+
React.createElement("button", { type: "button", "data-memory-engine-export": "true", disabled: busy, onClick: function () { manage(false); }, style: actionStyle }, "导出全部记忆(JSON)"),
|
|
907
|
+
React.createElement("p", { style: { margin: "10px 0 0", fontSize: "12px", color: "#9fb7ce" } }, "下载包含全部原始条目的 JSON 文件。导入已有记忆请使用 memory_import 工具。")),
|
|
908
|
+
React.createElement("h3", { style: { color: "#ffb8b8", marginTop: "24px" } }, "危险操作"),
|
|
909
|
+
React.createElement("p", { style: { color: "#9fb7ce" } }, "清空前会自动备份,最近五份操作备份保留在数据目录。"),
|
|
910
|
+
React.createElement("button", { type: "button", "data-memory-engine-clear": "true", disabled: busy,
|
|
911
|
+
onClick: function () { setError(""); setNotice(""); setConfirming(true); }, style: Object.assign({}, actionStyle, { color: "#ffb8b8", borderColor: "#8b505c", background: "#341c28" }) }, "清空全部记忆")),
|
|
912
|
+
confirming ? React.createElement(ConfirmationDialog, { title: "清空全部记忆", message: "这会删除当前记忆库的全部条目。请确认已导出需要保留的内容。", requireDelete: true,
|
|
913
|
+
submitLabel: "确认清空", busy: busy, error: error, onClose: function () { setConfirming(false); }, onConfirm: function () { manage(true); } }) : null);
|
|
914
|
+
}
|
|
915
|
+
|
|
491
916
|
function MemoryButton(props) {
|
|
492
917
|
var settings = props.settings;
|
|
493
918
|
var state = React.useState(false);
|
|
@@ -508,6 +933,8 @@ function frame(now) {
|
|
|
508
933
|
var selectedState = React.useState(null);
|
|
509
934
|
var selected = selectedState[0];
|
|
510
935
|
var setSelected = selectedState[1];
|
|
936
|
+
var tabState = React.useState("map");
|
|
937
|
+
var tab = tabState[0], setTab = tabState[1];
|
|
511
938
|
var renderer = React.useRef(null);
|
|
512
939
|
var selectionVersion = React.useRef(0);
|
|
513
940
|
var pointer = React.useRef(null);
|
|
@@ -522,6 +949,7 @@ function frame(now) {
|
|
|
522
949
|
dismissed.current = false;
|
|
523
950
|
setGuideOpen(false);
|
|
524
951
|
setSelected(null);
|
|
952
|
+
setTab("map");
|
|
525
953
|
setOpen(Date.now());
|
|
526
954
|
}
|
|
527
955
|
function closeMap() { setSelected(null); setOpen(false); }
|
|
@@ -562,6 +990,7 @@ function frame(now) {
|
|
|
562
990
|
function onEscape() { if (guideOpen) closeGuide(); else if (selected) setSelected(null); else closeMap(); }
|
|
563
991
|
function onKey(event) {
|
|
564
992
|
if (event.key !== "Escape") return;
|
|
993
|
+
if (event.target && event.target.closest && event.target.closest("[data-memory-engine-confirm]")) return;
|
|
565
994
|
event.preventDefault();
|
|
566
995
|
event.stopPropagation();
|
|
567
996
|
if (!event.repeat) onEscape();
|
|
@@ -585,12 +1014,20 @@ function frame(now) {
|
|
|
585
1014
|
onPointerDownCapture: startPointer, onPointerMoveCapture: movePointer,
|
|
586
1015
|
onPointerCancelCapture: function () { pointer.current = null; }, onClick: dismissOutside,
|
|
587
1016
|
style: { position: "fixed", inset: 0, maxWidth: "none", maxHeight: "none", zIndex: 9999, background: "#03060c" },
|
|
588
|
-
}, React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected })
|
|
1017
|
+
}, tab === "map" ? React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected })
|
|
1018
|
+
: tab === "memories" ? React.createElement(MemoryList, { onChanged: function () { setRetry(function (value) { return value + 1; }); } })
|
|
1019
|
+
: React.createElement(MemorySettings, { onCleared: function () { openMap(); setGuideOpen(true); } }),
|
|
1020
|
+
tab !== "map" ? React.createElement("strong", { style: { position: "absolute", top: "23px", left: "24px", font: "600 18px system-ui,sans-serif", color: "#b7e9ff" } }, "记忆管理") : null,
|
|
589
1021
|
React.createElement("style", null, "@keyframes dsh-memory-detail-in{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}}@media(prefers-reduced-motion:reduce){[data-memory-engine-detail]{animation:none!important}}"),
|
|
590
1022
|
React.createElement("div", {
|
|
591
1023
|
"data-memory-engine-controls": "true",
|
|
592
1024
|
style: { position: "absolute", top: "16px", right: "18px", zIndex: 1, display: "flex", gap: "8px" },
|
|
593
|
-
}, React.createElement("
|
|
1025
|
+
}, React.createElement("div", { role: "tablist", "aria-label": "记忆面板", style: { display: "flex", gap: "4px", marginRight: "8px" } },
|
|
1026
|
+
[["map", "图谱"], ["memories", "记忆"], ["settings", "设置"]].map(function (entry) {
|
|
1027
|
+
return React.createElement("button", { key: entry[0], type: "button", role: "tab", "aria-selected": tab === entry[0], "data-memory-engine-tab": entry[0],
|
|
1028
|
+
onClick: function () { setSelected(null); setTab(entry[0]); },
|
|
1029
|
+
style: Object.assign({}, actionStyle, { background: tab === entry[0] ? "#204762" : "#091422", borderColor: tab === entry[0] ? "#8acded" : "#35536b" }) }, entry[1]);
|
|
1030
|
+
})), React.createElement("button", {
|
|
594
1031
|
ref: help, type: "button", onClick: function () { setGuideOpen(true); },
|
|
595
1032
|
"aria-label": "查看使用指南", title: "使用指南", style: actionStyle,
|
|
596
1033
|
}, "?"), React.createElement("button", {
|