dsh-memory-engine 0.7.0-beta.2b → 0.7.0-beta.4

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/lib/client.js CHANGED
@@ -22,13 +22,14 @@ 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 = [];
26
26
  const byId = new Map(), adjacency = new Map(), sprites = new Map();
27
27
  const pulses = [], firing = new Map(), selections = new Set();
28
28
  if (typeof options.onSelect === 'function') selections.add(options.onSelect);
29
29
  let seed = 987654321;
30
30
  const random = () => ((seed = seed * 16807 % 2147483647) - 1) / 2147483646;
31
31
  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 spinSpeed=data.spinSpeed??0.35,resumeAt=0;
32
33
  let drag=null,nextFire=0,lastTime=0,rafId=null,running=false,destroyed=false,generation=0,observer=null;
33
34
 
34
35
  function glowSprite(rgb) {
@@ -43,8 +44,21 @@ function glowSprite(rgb) {
43
44
  return off;
44
45
  }
45
46
 
47
+ // 字符串 → 32 位无符号哈希。给连线的闪烁参数做种子:
48
+ // 用 id 而不是 Math.random,保证同一条线在每次打开时节奏一致(不会每次刷新都跳),
49
+ // 但不同连线之间互不同步。
50
+ function hashString(text) {
51
+ let hash = 2166136261;
52
+ for (let i = 0; i < text.length; i++) {
53
+ hash ^= text.charCodeAt(i);
54
+ hash = Math.imul(hash, 16777619);
55
+ }
56
+ return hash >>> 0;
57
+ }
58
+
46
59
  function setData(nextData) {
47
60
  if (destroyed) return;
61
+ spinSpeed=nextData.spinSpeed??0.35;
48
62
  nodes = nextData.nodes.map(node => ({ ...node, pos: { ...node.pos } }));
49
63
  links = nextData.links.map(link => [...link]);
50
64
  byId.clear(); adjacency.clear(); sprites.clear(); pulses.length = 0; firing.clear();
@@ -55,6 +69,24 @@ function glowSprite(rgb) {
55
69
  if (adjacency.has(left) && adjacency.has(right)) { adjacency.get(left).push(right); adjacency.get(right).push(left); }
56
70
  }
57
71
  hubs = nodes.filter(star => star.degree > 0).sort((left,right) => right.degree-left.degree).slice(0,6);
72
+ // 每条连线一份自己的闪烁参数:相位和频率都不一样,整体才不会"齐刷刷"地一起亮。
73
+ // 用连线两端 id 的哈希做种子,保证同一条线每次打开图的节奏一致(不会跳变),
74
+ // 但线与线之间互不同步。
75
+ linkTwinkle = links.map(([left, right]) => {
76
+ const h = hashString(left + '|' + right);
77
+ return {
78
+ // 频率调低:周期从 4~18 秒放慢到 18~55 秒,变成"慢慢呼吸"而不是"闪"
79
+ freq: 0.000114 + (h % 1000) / 1000 * 0.000235,
80
+ phase: (Math.floor(h / 1000) % 6283) / 1000, // 0 ~ 6.28,彼此错开
81
+ amp: 0.05 + (Math.floor(h / 7e6) % 100) / 100 * 0.12,
82
+ // 弯曲:一半往左弯一半往右弯,弯度 8%~24%。
83
+ // 之前所有线都朝同一侧弯,视觉上像漩涡,边界区域就显空。
84
+ bow: (h % 2 === 0 ? 1 : -1) * (0.08 + (Math.floor(h / 13) % 100) / 100 * 0.16),
85
+ // 端点偏移:同一个节点被很多线连时,让它们从不同角度进来,散成扇面而不是叠成一条
86
+ jitterStart: ((Math.floor(h / 31) % 100) / 100 - 0.5) * 1.6,
87
+ jitterEnd: ((Math.floor(h / 977) % 100) / 100 - 0.5) * 1.6,
88
+ };
89
+ });
58
90
  for (const star of nodes) {
59
91
  star._rgb = colors[star.kind] || fallback;
60
92
  const count = Math.min(9, 4 + Math.round(Math.sqrt(star.degree) * 1.2));
@@ -90,9 +122,19 @@ function project(pos) {
90
122
  }
91
123
  function fire(id,now) {
92
124
  firing.set(id,1);
93
- for (const other of adjacency.get(id)||[]) {
94
- firing.set(other,Math.max(0.65,firing.get(other)||0));
95
- pulses.push({from:id,to:other,born:now,life:900+Math.random()*700});
125
+ const neighbours=adjacency.get(id)||[];
126
+ // 不再"点亮整个扇面":只随机挑 2~4 条邻居。
127
+ // 一次把几十条边同时点亮,看上去就是全图一起闪。
128
+ const count=Math.min(neighbours.length,2+Math.floor(Math.random()*3));
129
+ const picked=new Set();
130
+ let guard=0;
131
+ while (picked.size<count && neighbours.length && guard++<200) {
132
+ picked.add(neighbours[Math.floor(Math.random()*neighbours.length)]);
133
+ }
134
+ for (const other of picked) {
135
+ // 邻居亮度也压低:原来 0.65 会让整片区域同时亮起来
136
+ firing.set(other,Math.max(0.26,firing.get(other)||0));
137
+ pulses.push({from:id,to:other,born:now,life:700+Math.random()*2400});
96
138
  }
97
139
  }
98
140
 
@@ -112,6 +154,7 @@ function fire(id,now) {
112
154
  }
113
155
 
114
156
  function onPointerDown(event) {
157
+ resumeAt=Infinity;
115
158
  autoSpin=false; drag={x:event.clientX,y:event.clientY,moved:0,pointerId:event.pointerId};
116
159
  canvas.setPointerCapture(event.pointerId);
117
160
  }
@@ -122,6 +165,7 @@ function fire(id,now) {
122
165
  rotY+=dx*0.006; rotX=Math.max(-1.5,Math.min(1.5,rotX+dy*0.006));
123
166
  }
124
167
  function releaseDrag() {
168
+ if (drag && running) resumeAt=performance.now()+2000;
125
169
  if (drag && canvas.hasPointerCapture(drag.pointerId)) canvas.releasePointerCapture(drag.pointerId);
126
170
  drag=null;
127
171
  }
@@ -140,13 +184,21 @@ function fire(id,now) {
140
184
 
141
185
  function frame(now) {
142
186
  const delta=Math.min(50,now-lastTime||16.7); lastTime=now;
143
- if (autoSpin) rotY+=0.0011*delta/16.7;
144
- if (now>nextFire) { nextFire=now+1000+Math.random()*1500; if (hubs.length) fire(hubs[Math.floor(Math.random()*hubs.length)].id,now); }
187
+ if (!autoSpin && !drag && now>=resumeAt) autoSpin=true;
188
+ if (autoSpin) rotY+=0.0011*spinSpeed*3*delta/16.7;
189
+ if (now>nextFire) {
190
+ // 心跳节奏别太机械:间隔随机,偶尔连发两个,偶尔安静久一点
191
+ const burst=Math.random()<0.22 ? 2 : 1;
192
+ for (let i=0;i<burst && hubs.length;i++) {
193
+ fire(hubs[Math.floor(Math.random()*hubs.length)].id,now);
194
+ }
195
+ nextFire=now+1400+Math.random()*3200;
196
+ }
145
197
  ctx.globalCompositeOperation='source-over'; ctx.fillStyle='#03060c'; ctx.fillRect(0,0,width,height);
146
198
  ctx.strokeStyle='rgba(40,90,140,0.055)';ctx.lineWidth=1;
147
199
  for (let index=0;index<20;index++) {
148
200
  const y=index/20*height;
149
- ctx.beginPath();ctx.moveTo(0,y);ctx.bezierCurveTo(width*.3,y+Math.sin(now*.0003+index)*30,width*.7,y-Math.cos(now*.00025+index)*30,width,y);ctx.stroke();
201
+ 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
202
  }
151
203
  const points=[];
152
204
  for (const star of nodes) {
@@ -154,19 +206,39 @@ function frame(now) {
154
206
  if (!p || p.x< -100 || p.x>width+100 || p.y< -100 || p.y>height+100) {star._screen=null;continue;}
155
207
  points.push({star,p});
156
208
  }
157
- points.sort((left,right)=>right.p.depth-left.p.depth);
209
+ points.sort((left,right)=>left.star.isCore?1:right.star.isCore?-1:right.p.depth-left.p.depth);
158
210
  const screen=new Map(points.map(point=>[point.star.id,point]));
159
211
  ctx.globalCompositeOperation='lighter';
160
- for (const [left,right] of links) {
212
+ for (let linkIndex=0;linkIndex<links.length;linkIndex++) {
213
+ const [left,right]=links[linkIndex];
161
214
  const first=screen.get(left),second=screen.get(right);
162
215
  if (!first||!second) continue;
163
216
  const act=Math.max(firing.get(left)||0,firing.get(right)||0);
164
- const alpha=Math.min(.85,.1*Math.min(first.star.retention,second.star.retention)+.35*act);
217
+ // 连线自己也会呼吸:每条线有独立相位和频率,不再"齐刷刷"地一起闪。
218
+ const twinkle=linkTwinkle[linkIndex];
219
+ const breathe=twinkle ? (0.5+0.5*Math.sin(now*twinkle.freq+twinkle.phase))*twinkle.amp : 0;
220
+ // 度数补偿:枢纽节点周围几十条线会糊成一块光斑,
221
+ // 按两端度数把这类线整体压暗,让"一条线"和"一堆线"的观感接近。
222
+ const degSum=(first.star.degree||0)+(second.star.degree||0);
223
+ const damp=1/(1+0.07*Math.max(0,degSum-6));
224
+ const alpha=Math.min(.85,(.1*Math.min(first.star.retention,second.star.retention)+.12*act+breathe)*damp);
165
225
  if (alpha<.01) continue;
166
- const mx=(first.p.x+second.p.x)/2+(first.p.y-second.p.y)*.18;
167
- const my=(first.p.y+second.p.y)/2+(second.p.x-first.p.x)*.18;
168
- ctx.strokeStyle='rgba(110,190,255,'+alpha+')';ctx.lineWidth=.7+act*1.6;
169
- ctx.beginPath();ctx.moveTo(first.p.x,first.p.y);ctx.quadraticCurveTo(mx,my,second.p.x,second.p.y);ctx.stroke();
226
+ // 端点沿法线错开,多条线进入同一节点时散成扇形
227
+ const dx=second.p.x-first.p.x, dy=second.p.y-first.p.y;
228
+ const len=Math.hypot(dx,dy)||1;
229
+ const nx=-dy/len, ny=dx/len;
230
+ const r1=Math.max(2,first.star.radius*first.p.scale*.55);
231
+ const r2=Math.max(2,second.star.radius*second.p.scale*.55);
232
+ const sx=first.p.x+nx*r1*0.9*(twinkle?twinkle.jitterStart:0);
233
+ const sy=first.p.y+ny*r1*0.9*(twinkle?twinkle.jitterStart:0);
234
+ const ex=second.p.x+nx*r2*0.9*(twinkle?twinkle.jitterEnd:0);
235
+ const ey=second.p.y+ny*r2*0.9*(twinkle?twinkle.jitterEnd:0);
236
+ // 弯曲方向和弯度都按连线各自的哈希来,避免所有线朝同一侧弯成漩涡
237
+ const bow=twinkle?twinkle.bow:0.18;
238
+ const mx=(sx+ex)/2+nx*len*bow*0.5;
239
+ const my=(sy+ey)/2+ny*len*bow*0.5;
240
+ ctx.strokeStyle='rgba(110,190,255,'+alpha+')';ctx.lineWidth=.7+act*1.6+breathe*1.4;
241
+ ctx.beginPath();ctx.moveTo(sx,sy);ctx.quadraticCurveTo(mx,my,ex,ey);ctx.stroke();
170
242
  }
171
243
  for (let index=pulses.length-1;index>=0;index--) {
172
244
  const pulse=pulses[index],fraction=(now-pulse.born)/pulse.life;
@@ -182,8 +254,25 @@ function frame(now) {
182
254
  for (const {star,p} of points) {
183
255
  const act=firing.get(star.id)||0,ret=Math.max(.05,Math.min(1.2,star.retention));
184
256
  const intensity=Math.min(1,.22+ret*.72)*(1+act*1.1);
185
- const radius=Math.max(2,star.radius*p.scale*.55)*(1+Math.sin(now*.0016+(star.pos.x+star.pos.z)*.3)*.06)*(1+act*.5);
257
+ // 每颗星自己的呼吸频率和相位:从 id 哈希来,避免全图同一个节拍
258
+ if (star._pulse===undefined) {
259
+ const h=hashString(star.id);
260
+ 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};
261
+ }
262
+ const pulse=star._pulse;
263
+ const breathe=Math.sin(now*pulse.freq+pulse.phase)*pulse.amp;
264
+ const radius=Math.max(2,star.radius*p.scale*.55)*(1+breathe)*(1+act*.5);
186
265
  const rgb=star._rgb.join(',');
266
+ if (star.isCore) {
267
+ const halo=ctx.createRadialGradient(p.x,p.y,radius*.75,p.x,p.y,radius*2.2);
268
+ halo.addColorStop(0,'rgba('+rgb+',0.12)');
269
+ halo.addColorStop(.65,'rgba('+rgb+',0.04)');
270
+ halo.addColorStop(1,'rgba('+rgb+',0)');
271
+ ctx.fillStyle=halo;ctx.beginPath();ctx.arc(p.x,p.y,radius*2.2,0,Math.PI*2);ctx.fill();
272
+ ctx.strokeStyle='rgba('+rgb+',0.38)';ctx.lineWidth=.7;ctx.setLineDash([3,5]);ctx.lineDashOffset=-now*.008;
273
+ ctx.beginPath();ctx.arc(p.x,p.y,radius*1.6,0,Math.PI*2);ctx.stroke();
274
+ ctx.setLineDash([]);ctx.lineDashOffset=0;
275
+ }
187
276
  ctx.strokeStyle='rgba('+rgb+','+Math.min(1,.3*intensity)+')';
188
277
  if (radius>2.5) for (const branch of star._dendrites) {
189
278
  const length=radius*2.2*branch.length,dx=Math.cos(branch.angle),dy=Math.sin(branch.angle);
@@ -218,7 +307,7 @@ function frame(now) {
218
307
  }
219
308
  function start() {
220
309
  if (running || destroyed) return;
221
- running=true; lastTime=0; nextFire=0;
310
+ running=true; lastTime=0; nextFire=0; autoSpin=true; resumeAt=0;
222
311
  const currentGeneration = ++generation;
223
312
  canvas.addEventListener('pointerdown',onPointerDown);
224
313
  canvas.addEventListener('pointermove',onPointerMove);
@@ -277,17 +366,21 @@ function frame(now) {
277
366
 
278
367
  var defaultSettings = { hideButton: false, showGuideOnEmpty: true };
279
368
  var actionStyle = { padding: "7px 12px", borderRadius: "8px", border: "1px solid #6999b8", background: "#102236", color: "#e2f4ff", font: "inherit", cursor: "pointer" };
369
+ var fieldStyle = { boxSizing: "border-box", padding: "10px 12px", borderRadius: "8px", border: "1px solid #35536b", background: "#091422", color: "#e2f4ff", font: "inherit", minWidth: 0 };
370
+ var kinds = ["知识", "印象", "对话", "事件", "日记"];
280
371
 
281
- function readApi(url, signal) {
282
- return fetch(url, { cache: "no-store", signal: signal }).then(function (response) {
283
- if (!response.ok) throw new Error("HTTP " + response.status);
284
- return response.json();
285
- }).then(function (result) {
286
- if (!result || result.ok !== true || !result.data || typeof result.data !== "object") throw new Error("接口返回格式无效");
287
- return result.data;
372
+ function requestApi(url, options) {
373
+ return fetch(url, Object.assign({ cache: "no-store" }, options)).then(function (response) {
374
+ return response.json().then(function (result) {
375
+ if (!response.ok || !result || result.ok !== true) throw new Error(result && result.error || "HTTP " + response.status);
376
+ if (!result.data || typeof result.data !== "object") throw new Error("接口返回格式无效");
377
+ return result.data;
378
+ });
288
379
  });
289
380
  }
290
381
 
382
+ function readApi(url, signal) { return requestApi(url, { signal: signal }); }
383
+
291
384
  function loadSettings() {
292
385
  var controller = new AbortController();
293
386
  var timer = setTimeout(function () { controller.abort(); }, 5000);
@@ -367,6 +460,8 @@ function frame(now) {
367
460
  function MemoryDetails(props) {
368
461
  var node = props.node;
369
462
  var text = typeof node.text === "string" ? node.text : "";
463
+ if (node.isCore) text = Array.from(text).slice(0, 4000).join("");
464
+ var defaultCopyLabel = node.isCore ? "复制全文" : "复制正文";
370
465
  var title = String(node.title || "").trim() || Array.from(text).slice(0, 20).join("");
371
466
  var date = new Date(node.ts);
372
467
  var day = Number.isFinite(date.getTime()) ? date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") : "未知";
@@ -379,7 +474,7 @@ function frame(now) {
379
474
  ];
380
475
  var emotion = Array.isArray(node.emotion) ? node.emotion.filter(function (item) { return item && item.tag; }).map(function (item) { return item.tag; }).join(" / ") : "";
381
476
  if (emotion) rows.push(["情绪", emotion]);
382
- var copiedState = React.useState("复制正文");
477
+ var copiedState = React.useState(defaultCopyLabel);
383
478
  var copyLabel = copiedState[0];
384
479
  var setCopyLabel = copiedState[1];
385
480
  var timer = React.useRef(null);
@@ -389,10 +484,10 @@ function frame(now) {
389
484
  copyVersion.current++;
390
485
  clearTimeout(timer.current);
391
486
  timer.current = null;
392
- setCopyLabel("复制正文");
487
+ setCopyLabel(defaultCopyLabel);
393
488
  if (body.current) body.current.scrollTop = 0;
394
489
  return function () { copyVersion.current++; clearTimeout(timer.current); timer.current = null; };
395
- }, [node.id, text]);
490
+ }, [node.id, text, defaultCopyLabel]);
396
491
  function copyBody() {
397
492
  var version = ++copyVersion.current;
398
493
  clearTimeout(timer.current);
@@ -400,7 +495,7 @@ function frame(now) {
400
495
  if (version !== copyVersion.current) return;
401
496
  setCopyLabel("已复制 ✓");
402
497
  timer.current = setTimeout(function () {
403
- if (version === copyVersion.current) setCopyLabel("复制正文");
498
+ if (version === copyVersion.current) setCopyLabel(defaultCopyLabel);
404
499
  timer.current = null;
405
500
  }, 1800);
406
501
  }, function () {
@@ -414,13 +509,13 @@ function frame(now) {
414
509
  React.createElement("span", { "data-memory-engine-detail-kind": "true", style: { color: "#a4e6ff", background: "#15344b", borderRadius: "6px", padding: "2px 8px", overflowWrap: "anywhere" } }, node.kind || "未分类"),
415
510
  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
511
  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 } },
512
+ 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
513
  rows.map(function (row) { return React.createElement(React.Fragment, { key: row[0] },
419
514
  React.createElement("dt", { style: { margin: 0 } }, row[0]),
420
515
  React.createElement("dd", { style: { margin: 0, overflowWrap: "anywhere" } }, row[1])); })),
421
516
  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
517
  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 }, "在图里高亮它"),
518
+ node.isCore ? null : React.createElement("button", { type: "button", "data-memory-engine-highlight": "true", style: actionStyle, onClick: props.onHighlight }, "在图里高亮它"),
424
519
  React.createElement("button", { type: "button", "data-memory-engine-copy-body": "true", style: actionStyle, onClick: copyBody }, copyLabel)));
425
520
  }
426
521
 
@@ -488,6 +583,297 @@ function frame(now) {
488
583
  React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null);
489
584
  }
490
585
 
586
+ function ConfirmationDialog(props) {
587
+ var valueState = React.useState("");
588
+ var value = valueState[0], setValue = valueState[1];
589
+ var errorState = React.useState("");
590
+ var error = errorState[0], setError = errorState[1];
591
+ var form = React.useRef(null);
592
+ React.useEffect(function () {
593
+ var active = document.activeElement;
594
+ var target = form.current && form.current.querySelector("input,button");
595
+ if (target) target.focus();
596
+ return function () { if (active && active.isConnected) active.focus(); };
597
+ }, []);
598
+ function onKey(event) {
599
+ if (event.key === "Escape") {
600
+ event.preventDefault(); event.stopPropagation();
601
+ if (!props.busy) props.onClose();
602
+ }
603
+ if (event.key === "Tab" && form.current) {
604
+ var controls = Array.from(form.current.querySelectorAll("input,button")).filter(function (control) { return !control.disabled; });
605
+ if (!controls.length) { event.preventDefault(); return; }
606
+ var index = controls.indexOf(document.activeElement);
607
+ if (event.shiftKey && index <= 0) { event.preventDefault(); controls[controls.length - 1].focus(); }
608
+ else if (!event.shiftKey && (index === -1 || index === controls.length - 1)) { event.preventDefault(); controls[0].focus(); }
609
+ }
610
+ }
611
+ return React.createElement("div", { "data-memory-engine-confirm": "true", onKeyDown: onKey,
612
+ style: { position: "fixed", inset: 0, zIndex: 3, display: "grid", placeItems: "center", padding: "24px", background: "#03060ccc" } },
613
+ React.createElement("form", { ref: form, role: "dialog", "aria-modal": "true", "aria-label": props.title,
614
+ onSubmit: function (event) {
615
+ event.preventDefault();
616
+ if (props.busy) return;
617
+ if (props.requireDelete && value !== "DELETE") { setError("请输入大写 DELETE,其他内容不会执行清空。"); return; }
618
+ setError(""); props.onConfirm();
619
+ },
620
+ style: { width: "min(460px,100%)", padding: "24px", border: "1px solid #8b505c", borderRadius: "16px", background: "#101a29", color: "#e2f4ff", boxShadow: "0 16px 80px #0008" } },
621
+ React.createElement("h2", { style: { margin: "0 0 12px", fontSize: "20px" } }, props.title),
622
+ React.createElement("p", { style: { color: "#c2cedc" } }, props.message),
623
+ props.requireDelete ? React.createElement("label", null, "输入 DELETE 确认",
624
+ React.createElement("input", { "aria-label": "输入 DELETE 确认", autoComplete: "off", value: value, disabled: props.busy,
625
+ onChange: function (event) { setValue(event.target.value); setError(""); }, style: Object.assign({}, fieldStyle, { display: "block", width: "100%", marginTop: "8px" }) })) : null,
626
+ error || props.error ? React.createElement("p", { role: "alert", style: { color: "#ffb8b8" } }, error || props.error) : null,
627
+ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: "10px", marginTop: "24px" } },
628
+ React.createElement("button", { type: "button", disabled: props.busy, onClick: props.onClose, style: actionStyle }, "取消"),
629
+ React.createElement("button", { type: "submit", disabled: props.busy, style: Object.assign({}, actionStyle, { borderColor: "#c86b7a", background: "#572a38" }) }, props.busy ? "正在处理…" : props.submitLabel))));
630
+ }
631
+
632
+ function MemoryRow(props) {
633
+ var item = props.item;
634
+ var expandedState = React.useState(false);
635
+ var expanded = expandedState[0], setExpanded = expandedState[1];
636
+ var detailState = React.useState(item);
637
+ var detail = detailState[0], setDetail = detailState[1];
638
+ var editState = React.useState(null);
639
+ var edit = editState[0], setEdit = editState[1];
640
+ var confirmState = React.useState(false);
641
+ var confirming = confirmState[0], setConfirming = confirmState[1];
642
+ var errorState = React.useState("");
643
+ var error = errorState[0], setError = errorState[1];
644
+ var loadingState = React.useState(false);
645
+ var loading = loadingState[0], setLoading = loadingState[1];
646
+ var busyState = React.useState(false);
647
+ var busy = busyState[0], setBusy = busyState[1];
648
+ var mutation = React.useRef(null);
649
+ React.useEffect(function () { return function () { if (mutation.current) mutation.current.abort(); }; }, []);
650
+ React.useEffect(function () {
651
+ if (!expanded) return;
652
+ var controller = new AbortController();
653
+ setLoading(true); setError("");
654
+ readApi("/memory-engine/api/item/" + encodeURIComponent(item.id), controller.signal).then(function (result) {
655
+ if (!controller.signal.aborted) setDetail(result);
656
+ }).catch(function (failure) {
657
+ if (!controller.signal.aborted) setError("读取详情失败:" + failure.message);
658
+ }).finally(function () { if (!controller.signal.aborted) setLoading(false); });
659
+ return function () { controller.abort(); };
660
+ }, [expanded, item.id]);
661
+ function mutate(method) {
662
+ if (mutation.current) return;
663
+ var controller = new AbortController();
664
+ mutation.current = controller;
665
+ setBusy(true); setError("");
666
+ var options = { method: method, signal: controller.signal };
667
+ if (method === "PUT") { options.headers = { "Content-Type": "application/json" }; options.body = JSON.stringify(edit); }
668
+ requestApi("/memory-engine/api/item/" + encodeURIComponent(item.id), options).then(function () {
669
+ if (controller.signal.aborted) return;
670
+ setEdit(null); setExpanded(false); setConfirming(false); props.onChanged();
671
+ }).catch(function (failure) {
672
+ if (!controller.signal.aborted) setError((method === "PUT" ? "保存失败:" : "删除失败:") + failure.message);
673
+ }).finally(function () {
674
+ if (!controller.signal.aborted) { mutation.current = null; setBusy(false); }
675
+ });
676
+ }
677
+ function change(field, value) { setEdit(function (previous) { var next = Object.assign({}, previous); next[field] = value; return next; }); }
678
+ var choices = kinds.includes(detail.kind) ? kinds : [detail.kind].concat(kinds);
679
+ return React.createElement("article", { "data-memory-engine-row": item.id,
680
+ style: { marginBottom: "12px", border: "1px solid " + (expanded ? "#4c85a4" : "#263f54"), borderRadius: "12px", background: "#0b1624", overflowWrap: "anywhere" } },
681
+ React.createElement("button", { type: "button", "data-memory-engine-expand": item.id, "aria-expanded": expanded, disabled: busy,
682
+ onClick: function () { setExpanded(!expanded); setEdit(null); setError(""); },
683
+ style: { width: "100%", display: "block", padding: "18px 20px", border: 0, borderRadius: "12px", background: "transparent", color: "inherit", textAlign: "left", font: "inherit", cursor: "pointer" } },
684
+ React.createElement("span", { style: { display: "flex", alignItems: "center", gap: "12px", flexWrap: "wrap" } },
685
+ React.createElement("span", { style: { color: "#a4e6ff", background: "#15344b", padding: "2px 8px", borderRadius: "6px", flexShrink: 0 } }, item.kind),
686
+ React.createElement("strong", { style: { flex: "1 1 180px", overflowWrap: "anywhere" } }, item.title),
687
+ React.createElement("span", { style: { color: "#8db0c7", fontSize: "12px", whiteSpace: "nowrap" } }, "留存 " + item.retention.toFixed(2) + " · " + item.degree + " 个连接")),
688
+ !expanded ? React.createElement("span", { style: { display: "block", color: "#9fb7ce", marginTop: "8px", overflowWrap: "anywhere" } }, item.preview) : null),
689
+ expanded ? React.createElement("div", { style: { padding: "0 20px 18px" } },
690
+ loading ? React.createElement("p", { role: "status" }, "正在读取全文…") : null,
691
+ edit ? React.createElement("form", { "data-memory-engine-editor": item.id, onSubmit: function (event) { event.preventDefault(); mutate("PUT"); } },
692
+ React.createElement("textarea", { "aria-label": "记忆正文", value: edit.text, disabled: busy,
693
+ onChange: function (event) { change("text", event.target.value); },
694
+ style: Object.assign({}, fieldStyle, { width: "100%", minHeight: "150px", maxHeight: "40vh", resize: "vertical", lineHeight: 1.7 }) }),
695
+ React.createElement("div", { style: { display: "flex", gap: "12px", flexWrap: "wrap", margin: "12px 0" } },
696
+ React.createElement("label", null, "分类 ", React.createElement("select", { "aria-label": "编辑分类", value: edit.kind, disabled: busy, style: fieldStyle,
697
+ onChange: function (event) { change("kind", event.target.value); } }, choices.map(function (value) { return React.createElement("option", { key: value, value: value }, value); }))),
698
+ React.createElement("label", null, "重要度 ", React.createElement("select", { "aria-label": "编辑重要度", value: edit.heat, disabled: busy, style: fieldStyle,
699
+ onChange: function (event) { change("heat", Number(event.target.value)); } },
700
+ [[1, "热(1)"], [2, "温(2)"], [3, "冷(3)"]].map(function (entry) { return React.createElement("option", { key: entry[0], value: entry[0] }, entry[1]); }))),
701
+ React.createElement("label", null, "相关人 ", React.createElement("input", { "aria-label": "编辑相关人", value: edit.who, disabled: busy,
702
+ onChange: function (event) { change("who", event.target.value); }, style: Object.assign({}, fieldStyle, { width: "140px" }) }))),
703
+ React.createElement("div", { style: { display: "flex", gap: "8px" } },
704
+ React.createElement("button", { type: "submit", disabled: busy, "data-memory-engine-save": "true", style: actionStyle }, busy ? "正在保存…" : "保存"),
705
+ React.createElement("button", { type: "button", disabled: busy, "data-memory-engine-cancel-edit": "true", style: actionStyle,
706
+ onClick: function () { setEdit(null); setExpanded(false); setError(""); } }, "取消")))
707
+ : React.createElement(React.Fragment, null,
708
+ React.createElement("div", { "data-memory-engine-full-text": "true", style: { whiteSpace: "pre-wrap", maxHeight: "40vh", overflowY: "auto", color: "#c2d3e3", marginBottom: "14px" } }, detail.text),
709
+ React.createElement("div", { style: { display: "flex", gap: "8px" } },
710
+ React.createElement("button", { type: "button", "data-memory-engine-edit": item.id, disabled: loading || busy, style: actionStyle,
711
+ onClick: function () { setError(""); setEdit({ text: detail.text, kind: detail.kind, heat: detail.heat, who: detail.who || "" }); } }, "编辑"),
712
+ React.createElement("button", { type: "button", "data-memory-engine-delete": item.id, disabled: loading || busy,
713
+ onClick: function () { setError(""); setConfirming(true); }, style: Object.assign({}, actionStyle, { color: "#ffb8b8", borderColor: "#8b505c" }) }, "删除"))),
714
+ error ? React.createElement("p", { role: "alert", style: { color: "#ffb8b8" } }, error) : null) : null,
715
+ confirming ? React.createElement(ConfirmationDialog, { title: "删除这条记忆", message: "确定删除这条记忆?不可撤销。操作前会保留备份。", submitLabel: "确定删除",
716
+ busy: busy, error: error, onClose: function () { setConfirming(false); }, onConfirm: function () { mutate("DELETE"); } }) : null);
717
+ }
718
+
719
+ function MemoryList(props) {
720
+ var searchState = React.useState("");
721
+ var search = searchState[0], setSearch = searchState[1];
722
+ var queryState = React.useState("");
723
+ var query = queryState[0], setQuery = queryState[1];
724
+ var kindState = React.useState("");
725
+ var kind = kindState[0], setKind = kindState[1];
726
+ var sortState = React.useState("recent");
727
+ var sort = sortState[0], setSort = sortState[1];
728
+ var offsetState = React.useState(0);
729
+ var offset = offsetState[0], setOffset = offsetState[1];
730
+ var pageState = React.useState({ items: [], total: 0, allTotal: 0 });
731
+ var page = pageState[0], setPage = pageState[1];
732
+ var loadingState = React.useState(true);
733
+ var loading = loadingState[0], setLoading = loadingState[1];
734
+ var errorState = React.useState("");
735
+ var error = errorState[0], setError = errorState[1];
736
+ var retryState = React.useState(0);
737
+ var retry = retryState[0], setRetry = retryState[1];
738
+ function refresh() {
739
+ setOffset(0); setRetry(function (value) { return value + 1; });
740
+ if (props.onChanged) props.onChanged();
741
+ }
742
+ React.useEffect(function () {
743
+ if (search === query) return;
744
+ var timer = setTimeout(function () { setOffset(0); setQuery(search); }, 250);
745
+ return function () { clearTimeout(timer); };
746
+ }, [search]);
747
+ React.useEffect(function () {
748
+ var controller = new AbortController();
749
+ var active = true;
750
+ setLoading(true); setError("");
751
+ var params = new URLSearchParams({ q: query, kind: kind, sort: sort, limit: "50", offset: String(offset) });
752
+ readApi("/memory-engine/api/list?" + params, controller.signal).then(function (result) {
753
+ if (!active) return;
754
+ setPage(function (previous) { return Object.assign({}, result, { items: offset === 0 ? result.items : previous.items.concat(result.items) }); });
755
+ }).catch(function (failure) {
756
+ if (active) setError("加载记忆失败:" + failure.message);
757
+ }).finally(function () { if (active) setLoading(false); });
758
+ return function () { active = false; controller.abort(); };
759
+ }, [query, kind, sort, offset, retry]);
760
+ return React.createElement("section", {
761
+ "aria-label": "记忆列表", "data-memory-engine-list": "true",
762
+ style: { position: "absolute", inset: "76px 0 0", display: "flex", flexDirection: "column", color: "#dbeaf8", font: "14px/1.6 system-ui,sans-serif" },
763
+ }, React.createElement("div", { style: { width: "min(980px,100%)", margin: "0 auto", padding: "0 24px 16px", display: "flex", gap: "12px", flexWrap: "wrap" } },
764
+ React.createElement("input", { type: "search", "aria-label": "搜索记忆", placeholder: "搜索正文或标题…", value: search,
765
+ onChange: function (event) { setSearch(event.target.value); }, style: Object.assign({}, fieldStyle, { flex: "1 1 220px" }) }),
766
+ React.createElement("select", { "aria-label": "分类筛选", value: kind, style: fieldStyle,
767
+ onChange: function (event) { setOffset(0); setKind(event.target.value); } },
768
+ [""].concat(kinds).map(function (value) { return React.createElement("option", { key: value, value: value }, value || "全部分类"); })),
769
+ React.createElement("select", { "aria-label": "记忆排序", value: sort, style: fieldStyle,
770
+ onChange: function (event) { setOffset(0); setSort(event.target.value); } },
771
+ [["recent", "最近"], ["oldest", "最老"], ["fading", "快忘了"], ["degree", "连接最多"]].map(function (entry) { return React.createElement("option", { key: entry[0], value: entry[0] }, entry[1]); }))),
772
+ React.createElement("div", { "data-memory-engine-list-scroll": "true", style: { flex: 1, minHeight: 0, overflowY: "auto", padding: "0 24px 24px" } },
773
+ React.createElement("div", { style: { maxWidth: "932px", margin: "0 auto" }, "aria-busy": loading },
774
+ error ? React.createElement("div", { role: "alert", style: { color: "#ffb8b8", padding: "16px" } }, error,
775
+ React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试")) : null,
776
+ page.items.map(function (item) {
777
+ return React.createElement(MemoryRow, { key: item.id, item: item, onChanged: refresh });
778
+ }),
779
+ !loading && !error && !page.items.length ? React.createElement("p", { role: "status", style: { padding: "24px", textAlign: "center", color: "#9fb7ce" } }, page.allTotal ? "没有匹配的记忆,试试其他关键词或分类。" : "还没有记忆,可以点右上角 ? 查看导入方法。") : null)),
780
+ React.createElement("div", { "data-memory-engine-list-total": "true", style: { padding: "14px", textAlign: "center", color: "#9fb7ce", flexShrink: 0, borderTop: "1px solid #263f54" } },
781
+ "共 " + page.total + " 条 · 已显示 " + page.items.length + " 条",
782
+ loading ? React.createElement("span", { role: "status" }, " · 正在加载…") : null,
783
+ page.items.length < page.total ? React.createElement("button", { type: "button", "data-memory-engine-more": "true", disabled: loading || search !== query || !!error,
784
+ onClick: function () { setOffset(page.items.length); }, style: Object.assign({}, actionStyle, { marginLeft: "12px", opacity: loading ? 0.5 : 1 }) }, "加载更多") : null));
785
+ }
786
+
787
+ function MemorySettings(props) {
788
+ var statsState = React.useState(null);
789
+ var stats = statsState[0], setStats = statsState[1];
790
+ var errorState = React.useState("");
791
+ var error = errorState[0], setError = errorState[1];
792
+ var noticeState = React.useState("");
793
+ var notice = noticeState[0], setNotice = noticeState[1];
794
+ var retryState = React.useState(0);
795
+ var retry = retryState[0], setRetry = retryState[1];
796
+ var busyState = React.useState(false);
797
+ var busy = busyState[0], setBusy = busyState[1];
798
+ var confirmState = React.useState(false);
799
+ var confirming = confirmState[0], setConfirming = confirmState[1];
800
+ var mutation = React.useRef(null);
801
+ var download = React.useRef(null);
802
+ var timer = React.useRef(null);
803
+ var active = React.useRef(true);
804
+ React.useEffect(function () {
805
+ active.current = true;
806
+ return function () {
807
+ active.current = false;
808
+ if (mutation.current) mutation.current.abort();
809
+ clearTimeout(timer.current);
810
+ if (download.current) URL.revokeObjectURL(download.current);
811
+ };
812
+ }, []);
813
+ React.useEffect(function () {
814
+ var controller = new AbortController();
815
+ setError("");
816
+ readApi("/memory-engine/api/stats", controller.signal).then(function (result) {
817
+ if (!controller.signal.aborted) setStats(result);
818
+ }).catch(function (failure) {
819
+ if (!controller.signal.aborted) setError("读取设置失败:" + failure.message);
820
+ });
821
+ return function () { controller.abort(); };
822
+ }, [retry]);
823
+ function manage(clearing) {
824
+ if (mutation.current) return;
825
+ var controller = new AbortController();
826
+ mutation.current = controller; setBusy(true); setError(""); setNotice("");
827
+ var options = { signal: controller.signal };
828
+ if (clearing) { options.method = "POST"; options.headers = { "Content-Type": "application/json" }; options.body = JSON.stringify({ confirm: "DELETE" }); }
829
+ requestApi("/memory-engine/api/" + (clearing ? "clear" : "export"), options).then(function (result) {
830
+ if (controller.signal.aborted) return;
831
+ if (clearing) { props.onCleared(); return; }
832
+ if (download.current) URL.revokeObjectURL(download.current);
833
+ clearTimeout(timer.current);
834
+ download.current = URL.createObjectURL(new Blob([JSON.stringify({ ok: true, data: result }, null, 2) + "\n"], { type: "application/json;charset=utf-8" }));
835
+ var anchor = document.createElement("a");
836
+ var date = new Date();
837
+ anchor.href = download.current;
838
+ anchor.download = "memories-" + date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") + ".json";
839
+ document.body.appendChild(anchor); anchor.click(); anchor.remove();
840
+ timer.current = setTimeout(function () { URL.revokeObjectURL(download.current); download.current = null; timer.current = null; }, 1000);
841
+ setNotice("已开始下载,共 " + result.total + " 条记忆。");
842
+ }).catch(function (failure) {
843
+ if (!controller.signal.aborted) setError((clearing ? "清空失败:" : "导出失败:") + failure.message);
844
+ }).finally(function () {
845
+ if (!controller.signal.aborted) { mutation.current = null; setBusy(false); }
846
+ });
847
+ }
848
+ var directory = stats ? stats.file.replace(/[\\/][^\\/]*$/, "") : "";
849
+ return React.createElement("section", { "aria-label": "记忆设置", "data-memory-engine-settings": "true",
850
+ style: { position: "absolute", inset: "76px 0 0", overflowY: "auto", padding: "0 24px 32px", color: "#dbeaf8", font: "14px/1.7 system-ui,sans-serif" } },
851
+ React.createElement("div", { style: { maxWidth: "900px", margin: "0 auto", padding: "28px", border: "1px solid #263f54", borderRadius: "14px", background: "#0b1624" } },
852
+ React.createElement("h2", { style: { margin: "0 0 20px", fontSize: "20px", color: "#b7e9ff" } }, "数据管理"),
853
+ stats ? React.createElement(React.Fragment, null,
854
+ React.createElement("div", { style: { display: "flex", alignItems: "center", flexWrap: "wrap", gap: "12px" } },
855
+ React.createElement("span", { style: { flex: "1 1 360px", overflowWrap: "anywhere" } }, "数据目录:", React.createElement("code", { "data-memory-engine-directory": "true" }, directory)),
856
+ React.createElement("button", { type: "button", "data-memory-engine-copy-path": "true", style: actionStyle,
857
+ onClick: function () {
858
+ setNotice(""); setError("");
859
+ copyText(directory).then(function () { if (active.current) setNotice("数据目录已复制 ✓"); }, function () { if (active.current) setError("复制路径失败,请手动复制上方目录。"); });
860
+ } }, "复制路径")),
861
+ React.createElement("p", { "data-memory-engine-storage-stats": "true", style: { color: "#9fb7ce", margin: "16px 0 24px" } }, "记忆条数:" + stats.total + "  文件大小:" + (stats.bytes / 1024).toFixed(1) + " KB"))
862
+ : React.createElement("p", { role: "status" }, error ? "暂时无法读取数据目录。" : "正在读取数据目录…"),
863
+ error ? React.createElement("div", { role: "alert", style: { color: "#ffb8b8", marginBottom: "16px" } }, error,
864
+ !stats ? React.createElement("button", { type: "button", style: actionStyle, onClick: function () { setRetry(function (value) { return value + 1; }); } }, "重试") : null) : null,
865
+ notice ? React.createElement("p", { role: "status", style: { color: "#9fe5c4" } }, notice) : null,
866
+ React.createElement("div", { style: { padding: "24px 0", borderTop: "1px solid #263f54", borderBottom: "1px solid #263f54" } },
867
+ React.createElement("button", { type: "button", "data-memory-engine-export": "true", disabled: busy, onClick: function () { manage(false); }, style: actionStyle }, "导出全部记忆(JSON)"),
868
+ React.createElement("p", { style: { margin: "10px 0 0", fontSize: "12px", color: "#9fb7ce" } }, "下载包含全部原始条目的 JSON 文件。导入已有记忆请使用 memory_import 工具。")),
869
+ React.createElement("h3", { style: { color: "#ffb8b8", marginTop: "24px" } }, "危险操作"),
870
+ React.createElement("p", { style: { color: "#9fb7ce" } }, "清空前会自动备份,最近五份操作备份保留在数据目录。"),
871
+ React.createElement("button", { type: "button", "data-memory-engine-clear": "true", disabled: busy,
872
+ onClick: function () { setError(""); setNotice(""); setConfirming(true); }, style: Object.assign({}, actionStyle, { color: "#ffb8b8", borderColor: "#8b505c", background: "#341c28" }) }, "清空全部记忆")),
873
+ confirming ? React.createElement(ConfirmationDialog, { title: "清空全部记忆", message: "这会删除当前记忆库的全部条目。请确认已导出需要保留的内容。", requireDelete: true,
874
+ submitLabel: "确认清空", busy: busy, error: error, onClose: function () { setConfirming(false); }, onConfirm: function () { manage(true); } }) : null);
875
+ }
876
+
491
877
  function MemoryButton(props) {
492
878
  var settings = props.settings;
493
879
  var state = React.useState(false);
@@ -508,6 +894,8 @@ function frame(now) {
508
894
  var selectedState = React.useState(null);
509
895
  var selected = selectedState[0];
510
896
  var setSelected = selectedState[1];
897
+ var tabState = React.useState("map");
898
+ var tab = tabState[0], setTab = tabState[1];
511
899
  var renderer = React.useRef(null);
512
900
  var selectionVersion = React.useRef(0);
513
901
  var pointer = React.useRef(null);
@@ -522,6 +910,7 @@ function frame(now) {
522
910
  dismissed.current = false;
523
911
  setGuideOpen(false);
524
912
  setSelected(null);
913
+ setTab("map");
525
914
  setOpen(Date.now());
526
915
  }
527
916
  function closeMap() { setSelected(null); setOpen(false); }
@@ -562,6 +951,7 @@ function frame(now) {
562
951
  function onEscape() { if (guideOpen) closeGuide(); else if (selected) setSelected(null); else closeMap(); }
563
952
  function onKey(event) {
564
953
  if (event.key !== "Escape") return;
954
+ if (event.target && event.target.closest && event.target.closest("[data-memory-engine-confirm]")) return;
565
955
  event.preventDefault();
566
956
  event.stopPropagation();
567
957
  if (!event.repeat) onEscape();
@@ -585,12 +975,20 @@ function frame(now) {
585
975
  onPointerDownCapture: startPointer, onPointerMoveCapture: movePointer,
586
976
  onPointerCancelCapture: function () { pointer.current = null; }, onClick: dismissOutside,
587
977
  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 }),
978
+ }, tab === "map" ? React.createElement(MemoryCanvas, { key: open, rendererRef: renderer, selectionVersion: selectionVersion, onSelect: setSelected })
979
+ : tab === "memories" ? React.createElement(MemoryList, { onChanged: function () { setRetry(function (value) { return value + 1; }); } })
980
+ : React.createElement(MemorySettings, { onCleared: function () { openMap(); setGuideOpen(true); } }),
981
+ tab !== "map" ? React.createElement("strong", { style: { position: "absolute", top: "23px", left: "24px", font: "600 18px system-ui,sans-serif", color: "#b7e9ff" } }, "记忆管理") : null,
589
982
  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
983
  React.createElement("div", {
591
984
  "data-memory-engine-controls": "true",
592
985
  style: { position: "absolute", top: "16px", right: "18px", zIndex: 1, display: "flex", gap: "8px" },
593
- }, React.createElement("button", {
986
+ }, React.createElement("div", { role: "tablist", "aria-label": "记忆面板", style: { display: "flex", gap: "4px", marginRight: "8px" } },
987
+ [["map", "图谱"], ["memories", "记忆"], ["settings", "设置"]].map(function (entry) {
988
+ return React.createElement("button", { key: entry[0], type: "button", role: "tab", "aria-selected": tab === entry[0], "data-memory-engine-tab": entry[0],
989
+ onClick: function () { setSelected(null); setTab(entry[0]); },
990
+ style: Object.assign({}, actionStyle, { background: tab === entry[0] ? "#204762" : "#091422", borderColor: tab === entry[0] ? "#8acded" : "#35536b" }) }, entry[1]);
991
+ })), React.createElement("button", {
594
992
  ref: help, type: "button", onClick: function () { setGuideOpen(true); },
595
993
  "aria-label": "查看使用指南", title: "使用指南", style: actionStyle,
596
994
  }, "?"), React.createElement("button", {