dsh-prime-memory 0.26.31 → 0.26.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -35900,6 +35900,7 @@ var __create = Object.create;
35900
35900
  // client/src/entry.tsx
35901
35901
  var entry_exports = {};
35902
35902
  __export(entry_exports, {
35903
+ EXPLORER_ID: () => EXPLORER_ID,
35903
35904
  SETTINGS_SEAT: () => SETTINGS_SEAT,
35904
35905
  apply: () => apply,
35905
35906
  inject: () => inject
@@ -40096,6 +40097,41 @@ var __create = Object.create;
40096
40097
  ] });
40097
40098
  }
40098
40099
 
40100
+ // client/src/explorer/entry-state.ts
40101
+ var wanted = true;
40102
+ var confirmed = false;
40103
+ var chosen = false;
40104
+ var listeners = /* @__PURE__ */ new Set();
40105
+ var notify = () => {
40106
+ for (const fn of listeners) fn(wanted);
40107
+ };
40108
+ var explorerEntryState = {
40109
+ /** 当前选择(仅在 confirmed 后有意义)。 */
40110
+ isWanted: () => wanted,
40111
+ /** 首读是否已完成(entry.tsx 决定何时开放挂载)。 */
40112
+ isConfirmed: () => confirmed,
40113
+ /** settings-get 首读落定:面板还没被拨过时采纳该值。 */
40114
+ confirm(value) {
40115
+ confirmed = true;
40116
+ if (chosen) return;
40117
+ wanted = value;
40118
+ notify();
40119
+ },
40120
+ /** 面板即时写入成功后调用:选择立刻生效(也是 confirmed)。 */
40121
+ set(value) {
40122
+ wanted = value;
40123
+ chosen = true;
40124
+ confirmed = true;
40125
+ notify();
40126
+ },
40127
+ subscribe(fn) {
40128
+ listeners.add(fn);
40129
+ return () => {
40130
+ listeners.delete(fn);
40131
+ };
40132
+ }
40133
+ };
40134
+
40099
40135
  // client/src/tabs/OverviewTab.tsx
40100
40136
  var import_jsx_runtime18 = require("react/jsx-runtime");
40101
40137
  function OverviewTab(props) {
@@ -40133,12 +40169,15 @@ var __create = Object.create;
40133
40169
  if (!r || !r.ok) {
40134
40170
  setSettingsData(prev);
40135
40171
  setError(r && r.error ? "开关写入失败:" + r.error.message : "开关写入失败");
40172
+ if (key === "explorerEnabled") explorerEntryState.set(prev.settings.explorerEnabled !== false);
40136
40173
  } else {
40137
40174
  setError(null);
40175
+ if (key === "explorerEnabled") explorerEntryState.set(value);
40138
40176
  }
40139
40177
  }).catch((e) => {
40140
40178
  setSettingsData(prev);
40141
40179
  setError("开关写入失败:" + String(e && e.message || e));
40180
+ if (key === "explorerEnabled") explorerEntryState.set(prev.settings.explorerEnabled !== false);
40142
40181
  });
40143
40182
  };
40144
40183
  const requestGraphOn = () => {
@@ -40209,6 +40248,7 @@ var __create = Object.create;
40209
40248
  const redistillEnabled = settingsData && settingsData.settings ? settingsData.settings.redistillEnabled === true : false;
40210
40249
  const mdSyncEnabled = settingsData && settingsData.settings ? settingsData.settings.mdSyncEnabled === true : false;
40211
40250
  const memportEnabled = settingsData && settingsData.settings ? settingsData.settings.memportEnabled === true : false;
40251
+ const explorerEnabled = settingsData && settingsData.settings ? settingsData.settings.explorerEnabled !== false : true;
40212
40252
  const bfConcurrency = settingsData && settingsData.settings ? settingsData.settings.backfillConcurrency ?? 0 : 0;
40213
40253
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
40214
40254
  settingsData && settingsData.supported === false ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { style: S.hint, children: "settings 服务不可用,记忆模式开关未启用(记忆保持全开)。" }) : settingsData ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: S.switchPanel, children: [
@@ -40301,6 +40341,18 @@ var __create = Object.create;
40301
40341
  }
40302
40342
  }
40303
40343
  ),
40344
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: S.panelLabel, children: "记忆库入口" }),
40345
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
40346
+ SwitchRow,
40347
+ {
40348
+ label: "侧栏「记忆库」入口",
40349
+ desc: explorerEnabled ? "已开启:左侧栏显示「记忆库」(L1 查询 + 关联跳转 + 2D/3D 记忆图);关闭即时隐藏,无需重启" : "已关闭:侧栏「记忆库」已移除;开启即时恢复,无需重启",
40350
+ checked: explorerEnabled,
40351
+ onChange: (v) => {
40352
+ toggle("explorerEnabled", v);
40353
+ }
40354
+ }
40355
+ ),
40304
40356
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: S.panelLabel, children: "工具封印" }),
40305
40357
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
40306
40358
  SwitchRow,
@@ -42496,6 +42548,43 @@ var __create = Object.create;
42496
42548
  return name;
42497
42549
  }
42498
42550
  var STAR_COUNT = 90;
42551
+ var EGO_SUBGRAPH_HOPS = 3;
42552
+ var EGO_SUBGRAPH_CAP = 400;
42553
+ function egoSubgraph2(full, rootId, hops = EGO_SUBGRAPH_HOPS, cap = EGO_SUBGRAPH_CAP) {
42554
+ const byId = new Map(full.nodes.map((n) => [n.id, n]));
42555
+ const root = byId.get(rootId);
42556
+ if (!root) return { nodes: [], edges: [], truncated: false };
42557
+ const adj = /* @__PURE__ */ new Map();
42558
+ for (const e of full.edges) {
42559
+ if (!adj.has(e.from)) adj.set(e.from, []);
42560
+ if (!adj.has(e.to)) adj.set(e.to, []);
42561
+ adj.get(e.from).push(e.to);
42562
+ adj.get(e.to).push(e.from);
42563
+ }
42564
+ const depth = /* @__PURE__ */ new Map([[rootId, 0]]);
42565
+ const order = [rootId];
42566
+ let cutByCap = false;
42567
+ for (let i = 0; i < order.length; i++) {
42568
+ const cur = order[i];
42569
+ const d = depth.get(cur);
42570
+ if (d >= hops) continue;
42571
+ for (const nb of adj.get(cur) ?? []) {
42572
+ if (depth.has(nb)) continue;
42573
+ if (order.length >= cap) {
42574
+ cutByCap = true;
42575
+ break;
42576
+ }
42577
+ depth.set(nb, d + 1);
42578
+ order.push(nb);
42579
+ }
42580
+ }
42581
+ const keep = new Set(order);
42582
+ return {
42583
+ nodes: order.map((id) => byId.get(id)),
42584
+ edges: full.edges.filter((e) => keep.has(e.from) && keep.has(e.to)),
42585
+ truncated: full.truncated || cutByCap
42586
+ };
42587
+ }
42499
42588
  var STAR_R_MIN = 0.12;
42500
42589
  var STAR_O_MIN = 0.12;
42501
42590
  function radiusForDegree(deg) {
@@ -42598,9 +42687,14 @@ var __create = Object.create;
42598
42687
  }
42599
42688
  function GraphCanvas(props) {
42600
42689
  const { full, emphasisId, fit = "center", height: canvasH = CANVAS_H } = props;
42690
+ const fillMode = canvasH === "fill";
42691
+ const realH = fillMode ? CANVAS_H : canvasH;
42601
42692
  const hostRef = (0, import_react24.useRef)(null);
42602
42693
  const pickRef = (0, import_react24.useRef)(props.onPick);
42603
42694
  pickRef.current = props.onPick;
42695
+ const emphasisRef = (0, import_react24.useRef)(emphasisId);
42696
+ emphasisRef.current = emphasisId;
42697
+ const graphRef = (0, import_react24.useRef)(null);
42604
42698
  const [hovered, setHovered] = (0, import_react24.useState)(null);
42605
42699
  const hideTimerRef = (0, import_react24.useRef)(null);
42606
42700
  const nodeKey = (0, import_react24.useMemo)(() => nodeKeyOf(full), [full]);
@@ -42636,7 +42730,7 @@ var __create = Object.create;
42636
42730
  const host = hostRef.current;
42637
42731
  if (!host) return;
42638
42732
  const width = host.clientWidth || 640;
42639
- const height = host.clientHeight || canvasH;
42733
+ const height = host.clientHeight || realH;
42640
42734
  const color = (type) => resolveColor(host, TYPE_TOKENS[type] ?? FALLBACK_TOKEN);
42641
42735
  const edgeColor = resolveColor(host, "--dsh-mem-border-strong");
42642
42736
  const causalColor = resolveColor(host, "--dsh-mem-accent");
@@ -42645,7 +42739,7 @@ var __create = Object.create;
42645
42739
  const bgColor = resolveColor(host, "--dsh-mem-bg-card");
42646
42740
  const strongColor = resolveColor(host, "--dsh-mem-text-1");
42647
42741
  const labelAll = full.edges.length <= LABEL_ALL_EDGES_MAX;
42648
- const isSeed = (d) => emphasisId !== void 0 && d.id === emphasisId;
42742
+ const isSeed = (d) => emphasisRef.current !== void 0 && d.id === emphasisRef.current;
42649
42743
  const data = toGraphData(full, width, height);
42650
42744
  const graph = new import_g6.Graph({
42651
42745
  container: host,
@@ -42726,6 +42820,7 @@ var __create = Object.create;
42726
42820
  });
42727
42821
  void graph.render().catch(() => {
42728
42822
  });
42823
+ graphRef.current = graph;
42729
42824
  graph.on("node:pointerenter", (evt) => {
42730
42825
  const id = evt.target?.id;
42731
42826
  if (!id) return;
@@ -42751,10 +42846,23 @@ var __create = Object.create;
42751
42846
  });
42752
42847
  return () => {
42753
42848
  if (!graph.destroyed) graph.destroy();
42849
+ graphRef.current = null;
42754
42850
  };
42755
- }, [full, nodeKey, emphasisId, fit, canvasH]);
42851
+ }, [full, nodeKey, fit, realH]);
42852
+ (0, import_react24.useEffect)(() => {
42853
+ const graph = graphRef.current;
42854
+ if (!graph || graph.destroyed) return;
42855
+ for (const n of full.nodes) {
42856
+ try {
42857
+ graph.updateNodeData([{ id: n.id, data: { name: n.name, type: n.type, degree: n.id === emphasisId ? -1 : void 0 } }]);
42858
+ } catch {
42859
+ }
42860
+ }
42861
+ void graph.draw().catch(() => {
42862
+ });
42863
+ }, [emphasisId, full]);
42756
42864
  const bubbleTip = hovered ? tips.get(hovered.id) : null;
42757
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { style: { position: "relative", width: "100%", height: canvasH, overflow: "hidden", borderRadius: 8 }, children: [
42865
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { style: { position: "relative", width: "100%", ...fillMode ? { flex: 1, minHeight: 0 } : { height: realH }, overflow: "hidden", borderRadius: 8 }, children: [
42758
42866
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
42759
42867
  "svg",
42760
42868
  {
@@ -44414,10 +44522,1562 @@ var __create = Object.create;
44414
44522
  ] });
44415
44523
  }
44416
44524
 
44525
+ // client/src/explorer/ExplorerPage.tsx
44526
+ var import_react33 = require("react");
44527
+
44528
+ // client/src/explorer/LinkPane.tsx
44529
+ var import_jsx_runtime30 = require("react/jsx-runtime");
44530
+ function relLine(n) {
44531
+ return `${n.relation}${n.causal ? "(因果)" : ""} · ${n.hop} 跳 · 经由 ${n.viaEntityId}`;
44532
+ }
44533
+ function LinkPane(props) {
44534
+ const { recordId, links, loading, error, onPickRecord, onEmphasisNode, labelOfRecord } = props;
44535
+ if (!recordId) {
44536
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { padding: 14, fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "从左侧选一条记忆,这里会列出它涉及的实体、这些实体的关系邻居、以及邻居们的来源记忆——可以一路跳下去。" });
44537
+ }
44538
+ if (error) {
44539
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { padding: 14, fontSize: 12, color: "var(--dsh-mem-accent)" }, children: [
44540
+ "关联查询失败:",
44541
+ error
44542
+ ] });
44543
+ }
44544
+ if (loading || !links) {
44545
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { padding: 14, fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: loading ? "加载中…" : " " });
44546
+ }
44547
+ const { record, entities, neighbors, records, supersede, truncated } = links;
44548
+ const sec = (title, count) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { fontSize: 11, letterSpacing: ".06em", color: "var(--dsh-mem-text-3)", margin: "12px 0 6px" }, children: [
44549
+ title,
44550
+ count != null ? ` (${count})` : ""
44551
+ ] });
44552
+ const chip = (id, text, title, onClick) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
44553
+ "button",
44554
+ {
44555
+ onClick,
44556
+ title,
44557
+ style: {
44558
+ display: "inline-block",
44559
+ margin: "0 6px 6px 0",
44560
+ padding: "1px 8px",
44561
+ borderRadius: 999,
44562
+ border: "1px solid var(--dsh-mem-border-strong)",
44563
+ background: "var(--dsh-mem-accent-weak)",
44564
+ color: "inherit",
44565
+ fontSize: 11,
44566
+ cursor: "pointer"
44567
+ },
44568
+ children: text
44569
+ },
44570
+ id + text
44571
+ );
44572
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { height: "100%", overflowY: "auto", paddingRight: 2 }, children: [
44573
+ sec("层1 · 当前记录"),
44574
+ record ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
44575
+ "div",
44576
+ {
44577
+ style: {
44578
+ border: "1px solid var(--dsh-mem-border)",
44579
+ borderRadius: 8,
44580
+ padding: "8px 10px",
44581
+ background: "var(--dsh-mem-bg-inset)",
44582
+ fontSize: 13,
44583
+ lineHeight: 1.6,
44584
+ opacity: record.retired ? 0.6 : 1
44585
+ },
44586
+ children: [
44587
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: record.content }),
44588
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", fontSize: 11, color: "var(--dsh-mem-text-3)", marginTop: 6 }, children: [
44589
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { children: record.type }),
44590
+ record.scene ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { children: record.scene }) : null,
44591
+ record.agingWeight != null ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { children: [
44592
+ "老化 ",
44593
+ Math.round(record.agingWeight * 100),
44594
+ "%"
44595
+ ] }) : null,
44596
+ record.maturity ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { children: [
44597
+ "成熟 ",
44598
+ record.maturity
44599
+ ] }) : null,
44600
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { children: fmtTime(record.updatedAt) }),
44601
+ record.retired ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { children: [
44602
+ "已退场",
44603
+ record.retiredReason ? `·${record.retiredReason}` : ""
44604
+ ] }) : null
44605
+ ] })
44606
+ ]
44607
+ }
44608
+ ) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "这条记忆还没有被投影进图(图谱未开启,或投影尚未跑到)——所以暂时查不到它的关联。" }),
44609
+ supersede.prevIds.length > 0 || supersede.nextId ? sec("取代链") : null,
44610
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { children: [
44611
+ supersede.nextId ? chip(supersede.nextId, `→ 被新记忆取代`, "取代本记录的新行,点击查看", () => onPickRecord(supersede.nextId)) : null,
44612
+ supersede.prevIds.map((id) => chip(id, `← 取代了旧记忆`, "本记录取代掉的旧行,点击查看", () => onPickRecord(id)))
44613
+ ] }),
44614
+ sec("层2 · 涉及实体", entities.length),
44615
+ entities.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "无(该记录没有抽出实体)。" }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { children: entities.map((e) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { children: [
44616
+ chip(e.id, `${e.name}`, `度 ${e.degree} · 因果 ${e.causalDegree} · 点击在图上强调`, () => onEmphasisNode(e.id)),
44617
+ e.recordIds.map(
44618
+ (rid) => chip(rid, `↳ ${labelOfRecord(rid).slice(0, 12)}`, "该实体的其它来源记忆,点击查看", () => onPickRecord(rid))
44619
+ )
44620
+ ] }, e.id)) }),
44621
+ sec("层3 · 关系邻居", neighbors.length),
44622
+ neighbors.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "无(这些实体目前没有 active 关系的邻居)。" }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: neighbors.map((n, i) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
44623
+ "div",
44624
+ {
44625
+ style: { border: "1px solid var(--dsh-mem-border)", borderRadius: 8, padding: "6px 8px" },
44626
+ children: [
44627
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "baseline", flexWrap: "wrap" }, children: [
44628
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { style: { fontSize: 13, fontWeight: 600 }, children: n.name }),
44629
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { style: { fontSize: 11, color: n.causal ? "var(--dsh-mem-accent)" : "var(--dsh-mem-text-3)" }, children: relLine(n) }),
44630
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(NButton, { onClick: () => onEmphasisNode(n.id), title: "在图上强调该节点", children: "看图" })
44631
+ ] }),
44632
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { marginTop: 4 }, children: n.recordIds.map(
44633
+ (rid) => chip(rid, `↳ ${labelOfRecord(rid).slice(0, 14)}`, "跳到这条邻居的来源记忆", () => onPickRecord(rid))
44634
+ ) })
44635
+ ]
44636
+ },
44637
+ `${n.id}-${n.relation}-${n.viaEntityId}-${i}`
44638
+ )) }),
44639
+ sec("相关记忆", records.length),
44640
+ records.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "无。" }) : records.map((r) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
44641
+ "button",
44642
+ {
44643
+ onClick: () => onPickRecord(r.id),
44644
+ style: {
44645
+ display: "block",
44646
+ width: "100%",
44647
+ textAlign: "left",
44648
+ padding: "6px 8px",
44649
+ marginBottom: 4,
44650
+ border: "1px solid var(--dsh-mem-border)",
44651
+ borderRadius: 8,
44652
+ background: "transparent",
44653
+ color: "inherit",
44654
+ cursor: "pointer",
44655
+ opacity: r.retired ? 0.55 : 1
44656
+ },
44657
+ children: [
44658
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { fontSize: 12, lineHeight: 1.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: r.content }),
44659
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: [
44660
+ r.type,
44661
+ r.agingWeight != null ? ` · 老化 ${Math.round(r.agingWeight * 100)}%` : "",
44662
+ " · ",
44663
+ fmtTime(r.updatedAt)
44664
+ ] })
44665
+ ]
44666
+ },
44667
+ r.id
44668
+ )),
44669
+ truncated ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-accent)", marginTop: 8 }, children: "邻居或相关记忆过多,此处已截断(不是没有更多)。" }) : null
44670
+ ] });
44671
+ }
44672
+
44673
+ // client/src/explorer/RecordPane.tsx
44674
+ var import_react31 = require("react");
44675
+ var import_jsx_runtime31 = require("react/jsx-runtime");
44676
+ var PAGE = 40;
44677
+ var CONDS0 = { query: "", type: "", retired: "" };
44678
+ function RecordPane(props) {
44679
+ const { rpc, activeId, onPick } = props;
44680
+ const [input, setInput] = (0, import_react31.useState)("");
44681
+ const [typeSel, setTypeSel] = (0, import_react31.useState)("");
44682
+ const [retiredSel, setRetiredSel] = (0, import_react31.useState)("");
44683
+ const [conds, setConds] = (0, import_react31.useState)(CONDS0);
44684
+ const [items, setItems] = (0, import_react31.useState)([]);
44685
+ const [hasMore, setHasMore] = (0, import_react31.useState)(false);
44686
+ const [total, setTotal] = (0, import_react31.useState)(null);
44687
+ const [truncated, setTruncated] = (0, import_react31.useState)(false);
44688
+ const [typeCatalog, setTypeCatalog] = (0, import_react31.useState)([]);
44689
+ const [loading, setLoading] = (0, import_react31.useState)(false);
44690
+ const [error, setError] = (0, import_react31.useState)(null);
44691
+ const offsetRef = (0, import_react31.useRef)(0);
44692
+ const seqRef = (0, import_react31.useRef)(0);
44693
+ const fetchPage = (0, import_react31.useCallback)(
44694
+ (next, offset, append) => {
44695
+ const token = ++seqRef.current;
44696
+ setLoading(true);
44697
+ setError(null);
44698
+ const payload = { limit: PAGE, offset };
44699
+ if (next.query) payload.query = next.query;
44700
+ if (next.type) payload.type = next.type;
44701
+ if (next.retired !== "") payload.retired = next.retired === "active";
44702
+ rpc("dsh-memory/list-records", payload).then((r) => {
44703
+ if (token !== seqRef.current) return;
44704
+ if (!r.ok) throw new Error(r.error.message);
44705
+ const v = r.value;
44706
+ setItems((prev) => append ? [...prev, ...v.items] : v.items);
44707
+ setHasMore(v.hasMore);
44708
+ setTotal(v.total);
44709
+ setTruncated(v.truncated);
44710
+ if (v.typeCatalog && offset === 0) setTypeCatalog(v.typeCatalog);
44711
+ offsetRef.current = offset + v.items.length;
44712
+ }).catch((e) => {
44713
+ if (token !== seqRef.current) return;
44714
+ setError(String(e?.message ?? e));
44715
+ }).finally(() => {
44716
+ if (token === seqRef.current) setLoading(false);
44717
+ });
44718
+ },
44719
+ [rpc]
44720
+ );
44721
+ const search = () => {
44722
+ const next = { query: input.trim(), type: typeSel, retired: retiredSel };
44723
+ setConds(next);
44724
+ fetchPage(next, 0, false);
44725
+ };
44726
+ (0, import_react31.useEffect)(() => {
44727
+ fetchPage(CONDS0, 0, false);
44728
+ }, [fetchPage]);
44729
+ const onKey = (e) => {
44730
+ if (e.key === "Enter") search();
44731
+ else if (e.key === "Escape") {
44732
+ setInput("");
44733
+ setTypeSel("");
44734
+ setRetiredSel("");
44735
+ setConds(CONDS0);
44736
+ fetchPage(CONDS0, 0, false);
44737
+ }
44738
+ };
44739
+ const labelOf = (t) => typeCatalog.find((c) => c.id === t)?.label ?? t;
44740
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { style: { display: "flex", flexDirection: "column", minHeight: 0, height: "100%" }, children: [
44741
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }, children: [
44742
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
44743
+ NInput,
44744
+ {
44745
+ style: { flex: "1 1 160px", minWidth: 140 },
44746
+ placeholder: "搜索记忆内容(关键词)…",
44747
+ value: input,
44748
+ onChange: (e) => setInput(e.target.value),
44749
+ onKeyDown: onKey
44750
+ }
44751
+ ),
44752
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
44753
+ "select",
44754
+ {
44755
+ className: "dsh-mem-input",
44756
+ value: typeSel,
44757
+ onChange: (e) => setTypeSel(e.target.value),
44758
+ title: "类型筛选",
44759
+ style: { maxWidth: 120 },
44760
+ children: [
44761
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("option", { value: "", children: "全部类型" }),
44762
+ typeCatalog.map((c) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("option", { value: c.id, children: c.label }, c.id))
44763
+ ]
44764
+ }
44765
+ ),
44766
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
44767
+ "select",
44768
+ {
44769
+ className: "dsh-mem-input",
44770
+ value: retiredSel,
44771
+ onChange: (e) => setRetiredSel(e.target.value),
44772
+ title: "退场筛选",
44773
+ style: { maxWidth: 96 },
44774
+ children: [
44775
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("option", { value: "", children: "全部" }),
44776
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("option", { value: "active", children: "仅活跃" }),
44777
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("option", { value: "retired", children: "仅退场" })
44778
+ ]
44779
+ }
44780
+ ),
44781
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(NButton, { onClick: search, title: "查询(Enter)", children: "查询" })
44782
+ ] }),
44783
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)", padding: "6px 2px" }, children: [
44784
+ total != null ? `共 ${total} 条 · ` : "",
44785
+ "已列 ",
44786
+ items.length,
44787
+ " 条",
44788
+ truncated ? " · 检索结果可能不完整" : "",
44789
+ loading ? " · 加载中…" : ""
44790
+ ] }),
44791
+ error && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { style: { fontSize: 12, color: "var(--dsh-mem-accent)", padding: "4px 0" }, children: [
44792
+ "查询失败:",
44793
+ error,
44794
+ " ",
44795
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(NButton, { onClick: () => fetchPage(conds, Math.max(0, offsetRef.current - items.length), false), children: "重试" })
44796
+ ] }),
44797
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { style: { flex: 1, minHeight: 0, overflowY: "auto", border: "1px solid var(--dsh-mem-border)", borderRadius: 8 }, children: [
44798
+ items.length === 0 && !loading && !error ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { style: { padding: 16, fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "没有符合条件的记忆。" }) : null,
44799
+ items.map((r) => {
44800
+ const on = r.id === activeId;
44801
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
44802
+ "button",
44803
+ {
44804
+ onClick: () => onPick(r.id),
44805
+ title: r.retired ? "已退场(软删,可恢复)" : void 0,
44806
+ style: {
44807
+ display: "block",
44808
+ width: "100%",
44809
+ textAlign: "left",
44810
+ padding: "8px 10px",
44811
+ borderBottom: "1px solid var(--dsh-mem-border)",
44812
+ background: on ? "var(--dsh-mem-accent-weak)" : "transparent",
44813
+ color: "inherit",
44814
+ cursor: "pointer",
44815
+ opacity: r.retired ? 0.55 : 1
44816
+ },
44817
+ children: [
44818
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
44819
+ "div",
44820
+ {
44821
+ style: {
44822
+ fontSize: 13,
44823
+ lineHeight: 1.5,
44824
+ display: "-webkit-box",
44825
+ WebkitLineClamp: 3,
44826
+ WebkitBoxOrient: "vertical",
44827
+ overflow: "hidden",
44828
+ textDecoration: r.retired ? "line-through" : "none"
44829
+ },
44830
+ children: r.content
44831
+ }
44832
+ ),
44833
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", fontSize: 11, color: "var(--dsh-mem-text-3)", marginTop: 4 }, children: [
44834
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { children: labelOf(r.type) }),
44835
+ r.scene ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { children: r.scene }) : null,
44836
+ r.agingWeight != null ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { title: "老化权重(读路径衰减系数)", children: [
44837
+ "老化 ",
44838
+ Math.round(r.agingWeight * 100),
44839
+ "%"
44840
+ ] }) : null,
44841
+ r.maturity ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { children: [
44842
+ "成熟 ",
44843
+ r.maturity
44844
+ ] }) : null,
44845
+ r.usage && r.usage.used > 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("span", { children: [
44846
+ "被用 ",
44847
+ r.usage.used
44848
+ ] }) : null,
44849
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { children: fmtTime(r.updatedAt) }),
44850
+ r.retired ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { children: "已退场" }) : null
44851
+ ] })
44852
+ ]
44853
+ },
44854
+ r.id
44855
+ );
44856
+ }),
44857
+ hasMore ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { style: { padding: 8, textAlign: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(NButton, { onClick: () => fetchPage(conds, offsetRef.current, true), disabled: loading, children: "加载更多" }) }) : null
44858
+ ] })
44859
+ ] });
44860
+ }
44861
+
44862
+ // client/src/explorer/Scene3D.tsx
44863
+ var import_react32 = require("react");
44864
+
44865
+ // client/src/explorer/projection.ts
44866
+ var FOCAL = 1100;
44867
+ var CAM_DEFAULT = { yaw: 0.45, pitch: -0.3, dist: 1200, panX: 0, panY: 0 };
44868
+ var DIST_MIN = 380;
44869
+ var DIST_MAX = 3200;
44870
+ var PITCH_LIMIT = 1.35;
44871
+ var ZOOM_STEP = 1.08;
44872
+ var PICK_PX = 16;
44873
+ var EGO_HOPS_MAX2 = 3;
44874
+ var SHELL_GAP = 300;
44875
+ var TRANSITION_MS = 520;
44876
+ var LABEL_CELL_W = 104;
44877
+ var LABEL_CELL_H = 14;
44878
+ var LABEL_MIN_F = 0.55;
44879
+ var LABEL_BUDGET_BASE = 72;
44880
+ var LABEL_BUDGET_MIN = 24;
44881
+ var LABEL_BUDGET_MAX = 280;
44882
+ var DEPTH_LIGHT_CAP = 0.85;
44883
+ var ALPHA_FLOOR = 0.06;
44884
+ var AGING_STRETCH_MIN = 0.2;
44885
+ function rand01(salt) {
44886
+ return hash322(salt) % 1e5 / 1e5;
44887
+ }
44888
+ function clusterOrder(types) {
44889
+ const seen = new Set(types);
44890
+ const known = ["person", "project", "organization", "tool", "place"].filter((t) => seen.has(t));
44891
+ const rest = [...seen].filter((t) => !known.includes(t)).sort((a, b) => hash322(a) - hash322(b));
44892
+ return [...known, ...rest];
44893
+ }
44894
+ function buildClusterLayout(nodes) {
44895
+ const out = /* @__PURE__ */ new Map();
44896
+ const types = clusterOrder(nodes.map((n) => n.type));
44897
+ const centers = /* @__PURE__ */ new Map();
44898
+ const total = Math.max(1, types.length);
44899
+ types.forEach((type, i) => {
44900
+ const y = total === 1 ? 0 : 1 - i / (total - 1) * 2;
44901
+ const ring = Math.sqrt(Math.max(0, 1 - y * y));
44902
+ const phi = i * Math.PI * (3 - Math.sqrt(5)) + rand01(`c:${type}`) * 0.6;
44903
+ const radius = 230 + rand01(`r:${type}`) * 190;
44904
+ centers.set(type, {
44905
+ x: Math.cos(phi) * ring * radius,
44906
+ y: y * radius * 0.82,
44907
+ // 纵向压扁:正球在俯视时上下层会互相挡住
44908
+ z: Math.sin(phi) * ring * radius
44909
+ });
44910
+ });
44911
+ for (const n of nodes) {
44912
+ const c = centers.get(n.type) ?? { x: 0, y: 0, z: 0 };
44913
+ const a = rand01(`a:${n.id}`) * Math.PI * 2;
44914
+ const b = Math.acos(2 * rand01(`b:${n.id}`) - 1);
44915
+ const rr = 16 + Math.pow(rand01(`s:${n.id}`), 0.65) * 120;
44916
+ out.set(n.id, {
44917
+ x: c.x + Math.sin(b) * Math.cos(a) * rr,
44918
+ y: c.y + Math.cos(b) * rr * 0.72,
44919
+ z: c.z + Math.sin(b) * Math.sin(a) * rr
44920
+ });
44921
+ }
44922
+ return out;
44923
+ }
44924
+ function buildEgoLayout(adj, focusId, hops) {
44925
+ const out = /* @__PURE__ */ new Map();
44926
+ const depth = Math.min(Math.max(Math.floor(hops), 1), EGO_HOPS_MAX2);
44927
+ const hopOf = /* @__PURE__ */ new Map([[focusId, 0]]);
44928
+ let frontier = [focusId];
44929
+ const byHop = /* @__PURE__ */ new Map();
44930
+ byHop.set(0, [focusId]);
44931
+ for (let h = 1; h <= depth; h++) {
44932
+ const next = [];
44933
+ for (const id of frontier) {
44934
+ for (const other of adj.get(id) ?? []) {
44935
+ if (hopOf.has(other)) continue;
44936
+ hopOf.set(other, h);
44937
+ next.push(other);
44938
+ }
44939
+ }
44940
+ if (next.length > 0) byHop.set(h, next);
44941
+ frontier = next;
44942
+ if (next.length === 0) break;
44943
+ }
44944
+ for (const [h, ids] of byHop) {
44945
+ const R2 = h * SHELL_GAP;
44946
+ const total = ids.length;
44947
+ ids.forEach((id, i) => {
44948
+ if (h === 0) {
44949
+ out.set(id, { x: 0, y: 0, z: 0 });
44950
+ return;
44951
+ }
44952
+ const y = total === 1 ? 0 : 1 - i / (total - 1) * 2;
44953
+ const ring = Math.sqrt(Math.max(0, 1 - y * y));
44954
+ const phi = i * Math.PI * (3 - Math.sqrt(5)) + rand01(`e:${id}`) * 0.7;
44955
+ const jitter = 1 + (rand01(`j:${id}`) - 0.5) * 0.18;
44956
+ out.set(id, {
44957
+ x: Math.cos(phi) * ring * R2 * jitter,
44958
+ y: y * R2 * 0.85 * jitter,
44959
+ z: Math.sin(phi) * ring * R2 * jitter
44960
+ });
44961
+ });
44962
+ }
44963
+ return out;
44964
+ }
44965
+ function projectAll(layout, cam, viewport, ids, t = 0, drift = true) {
44966
+ const cy = Math.cos(cam.yaw);
44967
+ const sy = Math.sin(cam.yaw);
44968
+ const cp = Math.cos(cam.pitch);
44969
+ const sp = Math.sin(cam.pitch);
44970
+ const out = [];
44971
+ const walk = (id) => {
44972
+ const p = layout.get(id);
44973
+ if (!p) return;
44974
+ const d = drift ? 1 : 0;
44975
+ const phase = rand01(`p:${id}`) * Math.PI * 2;
44976
+ const wx = p.x + Math.sin(t / 3200 + phase) * 7 * d + cam.panX;
44977
+ const wy = p.y + Math.cos(t / 4100 + phase) * 5 * d + cam.panY;
44978
+ const wz = p.z + Math.sin(t / 2700 + phase * 1.3) * 7 * d;
44979
+ const x1 = wx * cy - wz * sy;
44980
+ const z1 = wx * sy + wz * cy;
44981
+ const y1 = wy * cp - z1 * sp;
44982
+ const z2 = wy * sp + z1 * cp;
44983
+ const depth = cam.dist + z2;
44984
+ const f = FOCAL / Math.max(140, depth);
44985
+ out.push({ id, x: viewport.w / 2 + x1 * f, y: viewport.h / 2 + y1 * f, f, depth });
44986
+ };
44987
+ if (ids) for (const id of ids) walk(id);
44988
+ else for (const id of layout.keys()) walk(id);
44989
+ out.sort((a, b) => b.depth - a.depth);
44990
+ return out;
44991
+ }
44992
+ function brightnessFor(f, agingWeight, agingOn = true) {
44993
+ const depthLight = Math.min(DEPTH_LIGHT_CAP, 0.2 + f * 0.55);
44994
+ const rawAging = agingWeight == null ? 1 : agingWeight;
44995
+ const stretched = AGING_STRETCH_MIN >= 1 ? 1 : 0.12 + 0.88 * Math.max(0, Math.min(1, (rawAging - AGING_STRETCH_MIN) / (1 - AGING_STRETCH_MIN)));
44996
+ const agingLight = agingOn ? stretched : 1;
44997
+ return Math.max(ALPHA_FLOOR, depthLight * agingLight);
44998
+ }
44999
+ function labelBudget(camDist) {
45000
+ const raw = Math.round(LABEL_BUDGET_BASE * (CAM_DEFAULT.dist / Math.max(1, camDist)));
45001
+ return Math.min(LABEL_BUDGET_MAX, Math.max(LABEL_BUDGET_MIN, raw));
45002
+ }
45003
+ function pickLabels(candidates, budget, widthOf, viewportW, cellW = LABEL_CELL_W, cellH = LABEL_CELL_H) {
45004
+ const picked = [];
45005
+ const occupied = /* @__PURE__ */ new Set();
45006
+ for (const c of [...candidates].sort((a, b) => b.rank - a.rank)) {
45007
+ if (picked.length >= budget) break;
45008
+ if (c.f < LABEL_MIN_F) continue;
45009
+ const w = widthOf(c.id);
45010
+ const x = Math.round(c.x + 7);
45011
+ const y = Math.round(c.y - 5);
45012
+ if (x + w > viewportW - 4 || x < -2) continue;
45013
+ const cell = `${Math.floor(x / cellW)}:${Math.floor(y / cellH)}`;
45014
+ if (occupied.has(cell)) continue;
45015
+ occupied.add(cell);
45016
+ picked.push(c.id);
45017
+ }
45018
+ return picked;
45019
+ }
45020
+ function pickNearest(projected, mx, my, threshold = PICK_PX) {
45021
+ for (const p of projected) {
45022
+ if (Math.hypot(p.x - mx, p.y - my) < threshold) return p.id;
45023
+ }
45024
+ return null;
45025
+ }
45026
+ function easeInOut(p) {
45027
+ const k = Math.min(1, Math.max(0, p));
45028
+ return k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2;
45029
+ }
45030
+ function lerpLayout(from, to, k) {
45031
+ const e = easeInOut(k);
45032
+ const out = /* @__PURE__ */ new Map();
45033
+ for (const [id, b] of to) {
45034
+ const a = from.get(id) ?? b;
45035
+ out.set(id, { x: a.x + (b.x - a.x) * e, y: a.y + (b.y - a.y) * e, z: a.z + (b.z - a.z) * e });
45036
+ }
45037
+ return out;
45038
+ }
45039
+ function zoomBy(dist, deltaY) {
45040
+ const next = dist * (deltaY > 0 ? ZOOM_STEP : 1 / ZOOM_STEP);
45041
+ return Math.min(DIST_MAX, Math.max(DIST_MIN, next));
45042
+ }
45043
+
45044
+ // client/src/explorer/Scene3D.tsx
45045
+ var import_jsx_runtime32 = require("react/jsx-runtime");
45046
+ var TYPE_TOKENS2 = {
45047
+ person: "--dsh-mem-chart-1",
45048
+ project: "--dsh-mem-chart-2",
45049
+ organization: "--dsh-mem-chart-3",
45050
+ tool: "--dsh-mem-chart-4",
45051
+ place: "--dsh-mem-chart-5"
45052
+ };
45053
+ var FALLBACK_TOKEN2 = "--dsh-mem-chart-6";
45054
+ var FALLBACK_COLOR3 = "#8a8f98";
45055
+ function resolveColor2(host, token) {
45056
+ const v = getComputedStyle(host).getPropertyValue(token).trim();
45057
+ return v.length > 0 ? v : FALLBACK_COLOR3;
45058
+ }
45059
+ var SPRITE_PX = 64;
45060
+ function useLatest(fn) {
45061
+ const ref = (0, import_react32.useRef)(fn);
45062
+ ref.current = fn;
45063
+ return ((...args) => ref.current(...args));
45064
+ }
45065
+ function Scene3D(props) {
45066
+ const { full, onPick: onPickRaw, emphasisId, height = 620 } = props;
45067
+ const onPick = useLatest(onPickRaw);
45068
+ const hostRef = (0, import_react32.useRef)(null);
45069
+ const canvasRef = (0, import_react32.useRef)(null);
45070
+ const [glowOn, setGlowOn] = (0, import_react32.useState)(true);
45071
+ const [edgesOn, setEdgesOn] = (0, import_react32.useState)(true);
45072
+ const [labelsOn, setLabelsOn] = (0, import_react32.useState)(true);
45073
+ const [agingOn, setAgingOn] = (0, import_react32.useState)(true);
45074
+ const [pointSize, setPointSize] = (0, import_react32.useState)(2.2);
45075
+ const [driftOn, setDriftOn] = (0, import_react32.useState)(false);
45076
+ const [starry, setStarry] = (0, import_react32.useState)(true);
45077
+ const camRef = (0, import_react32.useRef)({ ...CAM_DEFAULT });
45078
+ const modeRef = (0, import_react32.useRef)("cluster");
45079
+ const focusRef = (0, import_react32.useRef)(null);
45080
+ const hoverRef = (0, import_react32.useRef)(null);
45081
+ const [hoverInfo, setHoverInfo] = (0, import_react32.useState)(null);
45082
+ const [mode, setMode] = (0, import_react32.useState)("cluster");
45083
+ const dragRef = (0, import_react32.useRef)(null);
45084
+ const clickRef = (0, import_react32.useRef)(null);
45085
+ const emphasisRef = (0, import_react32.useRef)(emphasisId);
45086
+ emphasisRef.current = emphasisId;
45087
+ const dirtyRef = (0, import_react32.useRef)(true);
45088
+ const degree = (0, import_react32.useMemo)(() => degreeMapOf(full), [full]);
45089
+ const edgeList = (0, import_react32.useMemo)(
45090
+ () => full.edges.map((e) => ({ from: e.from, to: e.to, relation: e.relation, causal: e.causal })),
45091
+ [full]
45092
+ );
45093
+ const adj = (0, import_react32.useMemo)(() => {
45094
+ const m = /* @__PURE__ */ new Map();
45095
+ for (const e of edgeList) {
45096
+ if (!m.has(e.from)) m.set(e.from, []);
45097
+ if (!m.has(e.to)) m.set(e.to, []);
45098
+ m.get(e.from).push(e.to);
45099
+ m.get(e.to).push(e.from);
45100
+ }
45101
+ return m;
45102
+ }, [edgeList]);
45103
+ const nodeById = (0, import_react32.useMemo)(() => new Map(full.nodes.map((n) => [n.id, n])), [full]);
45104
+ const nodeKeySeed = (0, import_react32.useMemo)(() => full.nodes.map((n) => n.id).join("|") || "empty", [full]);
45105
+ const clusterLayout = (0, import_react32.useMemo)(() => buildClusterLayout(full.nodes.map((n) => ({ id: n.id, type: n.type }))), [full]);
45106
+ const layoutRef = (0, import_react32.useRef)(clusterLayout);
45107
+ const egoLayoutRef = (0, import_react32.useRef)(null);
45108
+ const transRef = (0, import_react32.useRef)(null);
45109
+ (0, import_react32.useEffect)(() => {
45110
+ layoutRef.current = clusterLayout;
45111
+ egoLayoutRef.current = null;
45112
+ transRef.current = null;
45113
+ modeRef.current = "cluster";
45114
+ focusRef.current = null;
45115
+ setMode("cluster");
45116
+ }, [clusterLayout]);
45117
+ (0, import_react32.useEffect)(() => {
45118
+ if (!emphasisId || emphasisId === focusRef.current) return;
45119
+ if (!clusterLayout.has(emphasisId)) return;
45120
+ const egoL = buildEgoLayout(adj, emphasisId, EGO_HOPS_MAX2);
45121
+ egoLayoutRef.current = egoL;
45122
+ modeRef.current = "ego";
45123
+ focusRef.current = emphasisId;
45124
+ setMode("ego");
45125
+ transRef.current = { from: layoutRef.current, to: egoL, start: performance.now() };
45126
+ camRef.current.dist = Math.min(3200, Math.max(380, EGO_HOPS_MAX2 * 300 * 1.55));
45127
+ camRef.current.panX = 0;
45128
+ camRef.current.panY = 0;
45129
+ }, [emphasisId, adj, clusterLayout]);
45130
+ const metasRef = (0, import_react32.useRef)(/* @__PURE__ */ new Map());
45131
+ const spritesRef = (0, import_react32.useRef)(/* @__PURE__ */ new Map());
45132
+ (0, import_react32.useEffect)(() => {
45133
+ const host = hostRef.current;
45134
+ if (!host) return;
45135
+ const colorCache = /* @__PURE__ */ new Map();
45136
+ const colorOf = (type) => {
45137
+ const token = TYPE_TOKENS2[type] ?? FALLBACK_TOKEN2;
45138
+ let c = colorCache.get(token);
45139
+ if (c === void 0) {
45140
+ c = resolveColor2(host, token);
45141
+ colorCache.set(token, c);
45142
+ }
45143
+ return c;
45144
+ };
45145
+ metasRef.current = new Map(
45146
+ full.nodes.map((n) => {
45147
+ const deg = degree.get(n.id) ?? 0;
45148
+ return [
45149
+ n.id,
45150
+ {
45151
+ id: n.id,
45152
+ name: n.name,
45153
+ type: n.type,
45154
+ color: colorOf(n.type),
45155
+ degree: deg,
45156
+ // 度数 → 基础半径:sqrt 压缩(与 2D radiusForDegree 同一族形状,单位换成 3D 世界)
45157
+ base: pointSize * (1 + Math.sqrt(deg) * 0.28),
45158
+ aging: n.aging ?? null,
45159
+ source: n.source ?? null
45160
+ }
45161
+ ];
45162
+ })
45163
+ );
45164
+ }, [full, degree, pointSize]);
45165
+ (0, import_react32.useLayoutEffect)(() => {
45166
+ const host = hostRef.current;
45167
+ const canvas = canvasRef.current;
45168
+ if (!host || !canvas) return;
45169
+ const ctx = canvas.getContext("2d");
45170
+ if (!ctx) return;
45171
+ dirtyRef.current = true;
45172
+ const buffer = document.createElement("canvas");
45173
+ const bctx = buffer.getContext("2d");
45174
+ let raf = 0;
45175
+ let disposed = false;
45176
+ let vw = 0;
45177
+ let vh = 0;
45178
+ const dprRef = { current: 1 };
45179
+ const spriteFor = (color) => {
45180
+ const hit = spritesRef.current.get(color);
45181
+ if (hit) return hit;
45182
+ const s = document.createElement("canvas");
45183
+ s.width = SPRITE_PX;
45184
+ s.height = SPRITE_PX;
45185
+ const sc = s.getContext("2d");
45186
+ const half = SPRITE_PX / 2;
45187
+ const g = sc.createRadialGradient(half, half, 0, half, half, half);
45188
+ g.addColorStop(0, color);
45189
+ g.addColorStop(0.4, color);
45190
+ g.addColorStop(1, "rgba(0,0,0,0)");
45191
+ sc.fillStyle = g;
45192
+ sc.beginPath();
45193
+ sc.arc(half, half, half, 0, Math.PI * 2);
45194
+ sc.fill();
45195
+ spritesRef.current.set(color, s);
45196
+ return s;
45197
+ };
45198
+ let bgCache = null;
45199
+ let bgCacheW = 0;
45200
+ let bgCacheH = 0;
45201
+ let bgCacheStarry = null;
45202
+ const bakeBg = (w, h, starryMode, seedKey) => {
45203
+ const cv = document.createElement("canvas");
45204
+ cv.width = w;
45205
+ cv.height = h;
45206
+ const bc = cv.getContext("2d");
45207
+ if (starryMode) {
45208
+ const sky = bc.createLinearGradient(0, 0, 0, h);
45209
+ sky.addColorStop(0, "#0a1226");
45210
+ sky.addColorStop(1, "#04070f");
45211
+ bc.fillStyle = sky;
45212
+ bc.fillRect(0, 0, w, h);
45213
+ const count = Math.min(260, Math.floor(w * h / 12500));
45214
+ const rnd = (i, tag) => hash322(`${seedKey}:${tag}:${i}`) % 1e5 / 1e5;
45215
+ for (let i = 0; i < count; i++) {
45216
+ const x = rnd(i, "x") * w;
45217
+ const y = rnd(i, "y") * h;
45218
+ const t = rnd(i, "t");
45219
+ if (t < 0.05) {
45220
+ const halo = bc.createRadialGradient(x, y, 0, x, y, 5.5);
45221
+ halo.addColorStop(0, "rgba(214, 226, 255, 0.8)");
45222
+ halo.addColorStop(1, "rgba(214, 226, 255, 0)");
45223
+ bc.fillStyle = halo;
45224
+ bc.beginPath();
45225
+ bc.arc(x, y, 5.5, 0, Math.PI * 2);
45226
+ bc.fill();
45227
+ bc.fillStyle = "rgba(230, 238, 255, 0.95)";
45228
+ bc.beginPath();
45229
+ bc.arc(x, y, 1.1, 0, Math.PI * 2);
45230
+ bc.fill();
45231
+ } else if (t < 0.22) {
45232
+ bc.fillStyle = `rgba(196, 210, 240, ${0.34 + t * 1.2})`;
45233
+ bc.beginPath();
45234
+ bc.arc(x, y, 0.9, 0, Math.PI * 2);
45235
+ bc.fill();
45236
+ } else {
45237
+ bc.fillStyle = `rgba(168, 184, 220, ${0.1 + t * 0.9})`;
45238
+ bc.beginPath();
45239
+ bc.arc(x, y, 0.55, 0, Math.PI * 2);
45240
+ bc.fill();
45241
+ }
45242
+ }
45243
+ bc.globalAlpha = 1;
45244
+ } else {
45245
+ bc.fillStyle = resolveColor2(host, "--dsh-mem-bg-card");
45246
+ bc.fillRect(0, 0, w, h);
45247
+ }
45248
+ return cv;
45249
+ };
45250
+ const resize = () => {
45251
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
45252
+ dprRef.current = dpr;
45253
+ vw = host.clientWidth || 640;
45254
+ vh = host.clientHeight || host.offsetHeight || height;
45255
+ canvas.width = Math.round(vw * dpr);
45256
+ canvas.height = Math.round(vh * dpr);
45257
+ canvas.style.width = `${vw}px`;
45258
+ canvas.style.height = `${vh}px`;
45259
+ buffer.width = canvas.width;
45260
+ buffer.height = canvas.height;
45261
+ bctx.setTransform(dpr, 0, 0, dpr, 0, 0);
45262
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
45263
+ bgCache = null;
45264
+ };
45265
+ resize();
45266
+ const ro = new ResizeObserver(resize);
45267
+ ro.observe(host);
45268
+ const activeLayout = (now) => {
45269
+ const tr = transRef.current;
45270
+ if (!tr) return layoutRef.current;
45271
+ const k = (now - tr.start) / TRANSITION_MS;
45272
+ if (k >= 1) {
45273
+ layoutRef.current = tr.to;
45274
+ transRef.current = null;
45275
+ return tr.to;
45276
+ }
45277
+ return lerpLayout(tr.from, tr.to, k);
45278
+ };
45279
+ let lastProjected = [];
45280
+ const frame = (now) => {
45281
+ if (disposed) return;
45282
+ const cam = camRef.current;
45283
+ const layout = activeLayout(now);
45284
+ const ego = modeRef.current === "ego";
45285
+ const metas = metasRef.current;
45286
+ const hover = hoverRef.current;
45287
+ const focus = focusRef.current;
45288
+ let nbSet = null;
45289
+ if (hover) {
45290
+ nbSet = /* @__PURE__ */ new Set([hover, ...adj.get(hover) ?? []]);
45291
+ if (focus && focus !== hover) nbSet.add(focus);
45292
+ }
45293
+ const projected = projectAll(layout, cam, { w: vw, h: vh }, void 0, now, driftOn && !ego);
45294
+ lastProjected = projected;
45295
+ const c = bctx;
45296
+ c.clearRect(0, 0, vw, vh);
45297
+ if (!bgCache || bgCacheW !== vw || bgCacheH !== vh || bgCacheStarry !== starry) {
45298
+ bgCache = bakeBg(vw, vh, starry, nodeKeySeed);
45299
+ bgCacheW = vw;
45300
+ bgCacheH = vh;
45301
+ bgCacheStarry = starry;
45302
+ }
45303
+ c.drawImage(bgCache, 0, 0);
45304
+ const pos = new Map(projected.map((p) => [p.id, p]));
45305
+ if (edgesOn) {
45306
+ const edgeBase = resolveColor2(host, "--dsh-mem-border-strong");
45307
+ const causalC = resolveColor2(host, "--dsh-mem-accent");
45308
+ c.globalCompositeOperation = "source-over";
45309
+ for (const e of edgeList) {
45310
+ const a = pos.get(e.from);
45311
+ const b = pos.get(e.to);
45312
+ if (!a || !b) continue;
45313
+ const related = !hover || nbSet.has(e.from) && nbSet.has(e.to) && (e.from === hover || e.to === hover);
45314
+ if (hover && !related) continue;
45315
+ const avgF = (a.f + b.f) / 2;
45316
+ const alpha = Math.min(0.9, Math.max(0.05, avgF * 0.34));
45317
+ c.strokeStyle = e.causal ? causalC : edgeBase;
45318
+ c.globalAlpha = hover ? Math.min(0.9, alpha + 0.25) : alpha;
45319
+ c.lineWidth = e.causal ? 1.2 : 0.7;
45320
+ c.beginPath();
45321
+ c.moveTo(a.x, a.y);
45322
+ c.lineTo(b.x, b.y);
45323
+ c.stroke();
45324
+ }
45325
+ c.globalAlpha = 1;
45326
+ }
45327
+ for (const p of projected) {
45328
+ const m = metas.get(p.id);
45329
+ if (!m) continue;
45330
+ const isFocus = p.id === focus || p.id === emphasisRef.current;
45331
+ const isHover = p.id === hover;
45332
+ const dim = hover !== null && nbSet !== null && !nbSet.has(p.id) && !isFocus;
45333
+ let alpha = brightnessFor(p.f, agingOn ? m.aging : null, true);
45334
+ if (dim) alpha = Math.min(alpha, 0.12);
45335
+ const r = Math.max(0.8, m.base * p.f * 1.75 * (isFocus || isHover ? 1.9 : 1));
45336
+ if (glowOn) {
45337
+ c.globalCompositeOperation = "lighter";
45338
+ c.globalAlpha = alpha * 0.5;
45339
+ const sp = spriteFor(m.color);
45340
+ const gr = r * 3.4;
45341
+ c.drawImage(sp, p.x - gr, p.y - gr, gr * 2, gr * 2);
45342
+ }
45343
+ c.globalCompositeOperation = "source-over";
45344
+ c.globalAlpha = Math.min(1, alpha + 0.15);
45345
+ c.fillStyle = m.color;
45346
+ c.beginPath();
45347
+ c.arc(p.x, p.y, r, 0, Math.PI * 2);
45348
+ c.fill();
45349
+ }
45350
+ c.globalAlpha = 1;
45351
+ if (labelsOn) {
45352
+ const textColor = resolveColor2(host, "--dsh-mem-text-2");
45353
+ const inSet = /* @__PURE__ */ new Set();
45354
+ const cand = projected.filter((p) => metas.has(p.id)).map((p) => {
45355
+ const m = metas.get(p.id);
45356
+ const hot = p.id === focus || p.id === hover || p.id === emphasisRef.current;
45357
+ if (hot) inSet.add(p.id);
45358
+ return {
45359
+ id: p.id,
45360
+ x: p.x,
45361
+ y: p.y,
45362
+ f: p.f,
45363
+ rank: (hot ? 1e6 : 0) + m.degree * 10 + (m.aging ?? 1) * 5 + hash322(p.id) % 7
45364
+ };
45365
+ });
45366
+ const widthOf = (id) => {
45367
+ const m = metas.get(id);
45368
+ return m ? clipByWidth(m.name).length * 6.4 : 0;
45369
+ };
45370
+ const picked = /* @__PURE__ */ new Set([...pickLabels(cand, labelBudget(cam.dist), widthOf, vw), ...inSet]);
45371
+ c.globalCompositeOperation = "source-over";
45372
+ c.fillStyle = textColor;
45373
+ c.font = '11px system-ui, "Microsoft YaHei", sans-serif';
45374
+ for (const p of projected) {
45375
+ if (!picked.has(p.id)) continue;
45376
+ const m = metas.get(p.id);
45377
+ const dim = hover !== null && nbSet !== null && !nbSet.has(p.id);
45378
+ c.globalAlpha = dim ? 0.18 : Math.min(1, Math.max(0.35, p.f));
45379
+ c.fillText(clipByWidth(m.name), p.x + 7, p.y - 5);
45380
+ }
45381
+ c.globalAlpha = 1;
45382
+ }
45383
+ if (hover && edgesOn) {
45384
+ const causalC = resolveColor2(host, "--dsh-mem-accent");
45385
+ const edgeBase = resolveColor2(host, "--dsh-mem-border-strong");
45386
+ c.font = '10px system-ui, "Microsoft YaHei", sans-serif';
45387
+ const used = /* @__PURE__ */ new Set();
45388
+ const related = edgeList.filter((e) => (e.from === hover || e.to === hover) && pos.has(e.from) && pos.has(e.to)).sort((a, b) => Number(b.causal) - Number(a.causal));
45389
+ for (const e of related.slice(0, 12)) {
45390
+ const a = pos.get(e.from);
45391
+ const b = pos.get(e.to);
45392
+ const mx = (a.x + b.x) / 2;
45393
+ const my = (a.y + b.y) / 2;
45394
+ const cell = Math.floor(mx / 72) * 1e3 + Math.floor(my / 13);
45395
+ if (used.has(cell)) continue;
45396
+ used.add(cell);
45397
+ c.fillStyle = e.causal ? causalC : edgeBase;
45398
+ c.globalAlpha = 0.92;
45399
+ c.fillText(e.relation, mx, my);
45400
+ }
45401
+ c.globalAlpha = 1;
45402
+ }
45403
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
45404
+ ctx.drawImage(buffer, 0, 0);
45405
+ ctx.setTransform(dprRef.current, 0, 0, dprRef.current, 0, 0);
45406
+ raf = requestAnimationFrame(frame);
45407
+ };
45408
+ frame(performance.now());
45409
+ const toLocal = (ev) => {
45410
+ const rect = canvas.getBoundingClientRect();
45411
+ return { x: ev.clientX - rect.left, y: ev.clientY - rect.top };
45412
+ };
45413
+ const onDown = (ev) => {
45414
+ const pt = toLocal(ev);
45415
+ dragRef.current = { btn: ev.button, x: pt.x, y: pt.y, panning: ev.shiftKey || ev.button === 1 };
45416
+ };
45417
+ const onMove = (ev) => {
45418
+ const pt = toLocal(ev);
45419
+ const drag = dragRef.current;
45420
+ if (drag) {
45421
+ const cam = camRef.current;
45422
+ if (drag.panning) {
45423
+ cam.panX += (pt.x - drag.x) * (cam.dist / CAM_DEFAULT.dist);
45424
+ cam.panY += (pt.y - drag.y) * (cam.dist / CAM_DEFAULT.dist);
45425
+ } else {
45426
+ cam.yaw += (pt.x - drag.x) * 6e-3;
45427
+ cam.pitch = Math.min(PITCH_LIMIT, Math.max(-PITCH_LIMIT, cam.pitch + (pt.y - drag.y) * 6e-3));
45428
+ }
45429
+ drag.x = pt.x;
45430
+ drag.y = pt.y;
45431
+ dirtyRef.current = true;
45432
+ return;
45433
+ }
45434
+ const hit = pickNearest(lastProjected, pt.x, pt.y, PICK_PX);
45435
+ if (hit !== hoverRef.current) {
45436
+ hoverRef.current = hit;
45437
+ dirtyRef.current = true;
45438
+ setHoverInfo(hit ? { id: hit, x: pt.x, y: pt.y } : null);
45439
+ } else if (hit) {
45440
+ setHoverInfo({ id: hit, x: pt.x, y: pt.y });
45441
+ }
45442
+ };
45443
+ const onUp = (ev) => {
45444
+ const drag = dragRef.current;
45445
+ dragRef.current = null;
45446
+ if (!drag || drag.panning || ev.button !== 0) return;
45447
+ const pt = toLocal(ev);
45448
+ if (Math.hypot(pt.x - drag.x, pt.y - drag.y) > 4) return;
45449
+ const hit = pickNearest(lastProjected, pt.x, pt.y, PICK_PX);
45450
+ if (!hit) return;
45451
+ const now = Date.now();
45452
+ const prev = clickRef.current;
45453
+ if (prev && prev.id === hit && now - prev.at < 380) {
45454
+ onPick(hit);
45455
+ clickRef.current = null;
45456
+ return;
45457
+ }
45458
+ clickRef.current = { id: hit, at: now };
45459
+ onPick(hit);
45460
+ const ego = modeRef.current === "ego";
45461
+ const startTransition = (to) => {
45462
+ transRef.current = { from: layoutRef.current, to, start: performance.now() };
45463
+ };
45464
+ if (ego && hit === focusRef.current) {
45465
+ modeRef.current = "cluster";
45466
+ focusRef.current = null;
45467
+ setMode("cluster");
45468
+ startTransition(clusterLayout);
45469
+ camRef.current.dist = CAM_DEFAULT.dist;
45470
+ } else if (!ego) {
45471
+ const egoL = buildEgoLayout(adj, hit, EGO_HOPS_MAX2);
45472
+ egoLayoutRef.current = egoL;
45473
+ modeRef.current = "ego";
45474
+ focusRef.current = hit;
45475
+ setMode("ego");
45476
+ startTransition(egoL);
45477
+ camRef.current.dist = Math.min(DIST_MAX, Math.max(DIST_MIN, EGO_HOPS_MAX2 * SHELL_GAP * 1.55));
45478
+ camRef.current.panX = 0;
45479
+ camRef.current.panY = 0;
45480
+ } else {
45481
+ const egoL = buildEgoLayout(adj, hit, EGO_HOPS_MAX2);
45482
+ egoLayoutRef.current = egoL;
45483
+ focusRef.current = hit;
45484
+ startTransition(egoL);
45485
+ }
45486
+ };
45487
+ const onWheel = (ev) => {
45488
+ ev.preventDefault();
45489
+ camRef.current.dist = zoomBy(camRef.current.dist, ev.deltaY);
45490
+ dirtyRef.current = true;
45491
+ };
45492
+ const onKey = (ev) => {
45493
+ if (ev.key === "Enter" && hoverRef.current) onPick(hoverRef.current);
45494
+ if (ev.key === "Escape" && modeRef.current === "ego") {
45495
+ modeRef.current = "cluster";
45496
+ focusRef.current = null;
45497
+ setMode("cluster");
45498
+ transRef.current = { from: layoutRef.current, to: clusterLayout, start: performance.now() };
45499
+ camRef.current.dist = CAM_DEFAULT.dist;
45500
+ }
45501
+ };
45502
+ const onLeave = () => {
45503
+ hoverRef.current = null;
45504
+ setHoverInfo(null);
45505
+ dragRef.current = null;
45506
+ };
45507
+ canvas.addEventListener("mousedown", onDown);
45508
+ window.addEventListener("mousemove", onMove);
45509
+ window.addEventListener("mouseup", onUp);
45510
+ canvas.addEventListener("wheel", onWheel, { passive: false });
45511
+ canvas.addEventListener("mouseleave", onLeave);
45512
+ window.addEventListener("keydown", onKey);
45513
+ return () => {
45514
+ disposed = true;
45515
+ cancelAnimationFrame(raf);
45516
+ ro.disconnect();
45517
+ canvas.removeEventListener("mousedown", onDown);
45518
+ window.removeEventListener("mousemove", onMove);
45519
+ window.removeEventListener("mouseup", onUp);
45520
+ canvas.removeEventListener("wheel", onWheel);
45521
+ canvas.removeEventListener("mouseleave", onLeave);
45522
+ window.removeEventListener("keydown", onKey);
45523
+ };
45524
+ }, [full, edgeList, adj, clusterLayout, glowOn, edgesOn, labelsOn, agingOn, driftOn, starry, nodeKeySeed, height, onPick]);
45525
+ const hoverMeta = hoverInfo ? metasRef.current.get(hoverInfo.id) : null;
45526
+ const ui = starry ? {
45527
+ barBg: "rgba(10, 13, 22, 0.72)",
45528
+ border: "1px solid rgba(120, 160, 255, 0.22)",
45529
+ text: "#c8d2e8",
45530
+ dim: "#93a3c4",
45531
+ accent: "#7fb1ff"
45532
+ } : {
45533
+ barBg: "var(--dsh-mem-bg-card)",
45534
+ border: "1px solid var(--dsh-mem-border)",
45535
+ text: "var(--dsh-mem-text-2)",
45536
+ dim: "var(--dsh-mem-text-3)",
45537
+ accent: "var(--dsh-mem-accent)"
45538
+ };
45539
+ const toggle = (label, on, set) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
45540
+ NButton,
45541
+ {
45542
+ onClick: () => set(!on),
45543
+ style: on ? { color: ui.accent, borderColor: ui.accent } : { color: ui.text, borderColor: ui.border },
45544
+ title: label,
45545
+ children: label
45546
+ }
45547
+ );
45548
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { ref: hostRef, style: { position: "relative", width: "100%", flex: 1, minHeight: height, overflow: "hidden", background: starry ? "#04070f" : "var(--dsh-mem-bg-card, #ffffff)" }, children: [
45549
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("canvas", { ref: canvasRef, style: { display: "block", cursor: dragRef.current ? "grabbing" : "grab", background: starry ? "#04070f" : "var(--dsh-mem-bg-card, #ffffff)" } }),
45550
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
45551
+ "div",
45552
+ {
45553
+ style: { position: "absolute", top: 8, left: 8, fontSize: 11, color: ui.dim, background: ui.barBg, border: ui.border, borderRadius: 8, padding: "4px 8px", pointerEvents: "none" },
45554
+ children: mode === "ego" ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { children: [
45555
+ "ego 壳层 · ",
45556
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("b", { style: { color: ui.accent }, children: (focusRef.current ? nodeById.get(focusRef.current)?.name : null) ?? "焦点" }),
45557
+ " 在第 0 层,第 k 跳在第 k 层",
45558
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("br", {}),
45559
+ "点焦点/Esc 回全图 · 双击/Enter 打开记录"
45560
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { children: "左拖旋转 · shift 拖平移 · 滚轮缩放 · 单击聚焦成 ego · 双击开记录" })
45561
+ }
45562
+ ),
45563
+ hoverInfo && hoverMeta ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
45564
+ "div",
45565
+ {
45566
+ style: {
45567
+ position: "absolute",
45568
+ left: Math.min(hoverInfo.x + 14, (hostRef.current?.clientWidth ?? 640) - 230),
45569
+ top: Math.max(6, hoverInfo.y - 10),
45570
+ maxWidth: 220,
45571
+ fontSize: 11.5,
45572
+ lineHeight: 1.5,
45573
+ background: "var(--dsh-mem-bg-card)",
45574
+ border: "1px solid var(--dsh-mem-border)",
45575
+ borderRadius: 8,
45576
+ padding: "6px 9px",
45577
+ pointerEvents: "none",
45578
+ color: "var(--dsh-mem-text-2)"
45579
+ },
45580
+ children: [
45581
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { style: { fontWeight: 600, color: "var(--dsh-mem-text-1)" }, children: hoverMeta.name }),
45582
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { children: [
45583
+ hoverMeta.type,
45584
+ " · 关系 ",
45585
+ hoverMeta.degree,
45586
+ " 条",
45587
+ hoverMeta.aging != null ? ` · 老化 ${hoverMeta.aging.toFixed(2)}` : ""
45588
+ ] }),
45589
+ hoverMeta.source ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { color: "var(--dsh-mem-text-3)" }, children: [
45590
+ "出处:",
45591
+ hoverMeta.source
45592
+ ] }) : null
45593
+ ]
45594
+ }
45595
+ ) : null,
45596
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
45597
+ "div",
45598
+ {
45599
+ style: { position: "absolute", bottom: 8, left: 8, display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap", background: ui.barBg, border: ui.border, borderRadius: 10, padding: "4px 8px" },
45600
+ children: [
45601
+ toggle("辉光", glowOn, setGlowOn),
45602
+ toggle("边", edgesOn, setEdgesOn),
45603
+ toggle("标签", labelsOn, setLabelsOn),
45604
+ toggle("老化亮度", agingOn, setAgingOn),
45605
+ toggle("漂移", driftOn, setDriftOn),
45606
+ toggle(starry ? "星空底" : "白底", starry, setStarry),
45607
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("label", { style: { fontSize: 11, color: ui.dim, display: "inline-flex", alignItems: "center", gap: 4 }, children: [
45608
+ "点",
45609
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("input", { type: "range", min: 1.2, max: 4, step: 0.2, value: pointSize, onChange: (e) => setPointSize(Number(e.target.value)) })
45610
+ ] }),
45611
+ mode === "ego" ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(NButton, { onClick: () => window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })), title: "回到全图", style: { color: ui.text, borderColor: ui.border }, children: "回全图" }) : null
45612
+ ]
45613
+ }
45614
+ )
45615
+ ] });
45616
+ }
45617
+
45618
+ // client/src/explorer/ExplorerPage.tsx
45619
+ var import_jsx_runtime33 = require("react/jsx-runtime");
45620
+ var DETAIL_MIN = 260;
45621
+ var DETAIL_DEFAULT = 380;
45622
+ var DETAIL_MAX_RATIO = 0.55;
45623
+ var LEGEND_TYPES = [
45624
+ ["person", "人", "--dsh-mem-chart-1"],
45625
+ ["project", "项目", "--dsh-mem-chart-2"],
45626
+ ["organization", "组织", "--dsh-mem-chart-3"],
45627
+ ["tool", "工具", "--dsh-mem-chart-4"],
45628
+ ["place", "地点", "--dsh-mem-chart-5"]
45629
+ ];
45630
+ function ExplorerPage(props) {
45631
+ const { rpc, onClose, canvasHeight = 620 } = props;
45632
+ const [full, setFull] = (0, import_react33.useState)(null);
45633
+ const [stats, setStats] = (0, import_react33.useState)(null);
45634
+ const [graphError, setGraphError] = (0, import_react33.useState)(null);
45635
+ const [graphLoading, setGraphLoading] = (0, import_react33.useState)(true);
45636
+ const [recordId, setRecordId] = (0, import_react33.useState)(null);
45637
+ const [links, setLinks] = (0, import_react33.useState)(null);
45638
+ const [linksLoading, setLinksLoading] = (0, import_react33.useState)(false);
45639
+ const [linksError, setLinksError] = (0, import_react33.useState)(null);
45640
+ const [emphasisId, setEmphasisId] = (0, import_react33.useState)(void 0);
45641
+ const [view, setView] = (0, import_react33.useState)("3d");
45642
+ const [egoRoot, setEgoRoot] = (0, import_react33.useState)(null);
45643
+ const [searchText, setSearchText] = (0, import_react33.useState)("");
45644
+ const [searchMiss, setSearchMiss] = (0, import_react33.useState)(false);
45645
+ const [searchOpen, setSearchOpen] = (0, import_react33.useState)(false);
45646
+ const linksSeq = (0, import_react33.useRef)(0);
45647
+ const [mode, setMode] = (0, import_react33.useState)("query");
45648
+ const [entryPane, setEntryPane] = (0, import_react33.useState)(false);
45649
+ const [detailOpen, setDetailOpen] = (0, import_react33.useState)(false);
45650
+ const [detailW, setDetailW] = (0, import_react33.useState)(DETAIL_DEFAULT);
45651
+ const containerRef = (0, import_react33.useRef)(null);
45652
+ const dragWRef = (0, import_react33.useRef)(null);
45653
+ const modeRef = (0, import_react33.useRef)(mode);
45654
+ modeRef.current = mode;
45655
+ const startDrag = (0, import_react33.useCallback)((ev) => {
45656
+ ev.preventDefault();
45657
+ dragWRef.current = { startX: ev.clientX, startW: detailW };
45658
+ const onMove = (e) => {
45659
+ const d = dragWRef.current;
45660
+ if (!d) return;
45661
+ const container = containerRef.current;
45662
+ const maxW = container ? Math.round(container.clientWidth * DETAIL_MAX_RATIO) : 600;
45663
+ setDetailW(Math.min(maxW, Math.max(DETAIL_MIN, d.startW - (e.clientX - d.startX))));
45664
+ };
45665
+ const onUp = () => {
45666
+ dragWRef.current = null;
45667
+ window.removeEventListener("mousemove", onMove);
45668
+ window.removeEventListener("mouseup", onUp);
45669
+ };
45670
+ window.addEventListener("mousemove", onMove);
45671
+ window.addEventListener("mouseup", onUp);
45672
+ }, [detailW]);
45673
+ const labels = (0, import_react33.useRef)(/* @__PURE__ */ new Map());
45674
+ const loadGraph = (0, import_react33.useCallback)(() => {
45675
+ setGraphLoading(true);
45676
+ setGraphError(null);
45677
+ setEgoRoot(null);
45678
+ rpc("dsh-memory/graph-preview", { mode: "full" }).then((r) => {
45679
+ if (!r.ok) throw new Error(r.error.message);
45680
+ setStats(r.value.stats);
45681
+ setFull(r.value.full ?? { nodes: [], edges: [], truncated: false });
45682
+ }).catch((e) => setGraphError(String(e?.message ?? e))).finally(() => setGraphLoading(false));
45683
+ }, [rpc]);
45684
+ (0, import_react33.useEffect)(() => {
45685
+ loadGraph();
45686
+ }, [loadGraph]);
45687
+ const remember = (0, import_react33.useCallback)((items) => {
45688
+ for (const it of items) if (it.content) labels.current.set(it.id, it.content);
45689
+ }, []);
45690
+ const openRecord = (0, import_react33.useCallback)(
45691
+ (id, opts) => {
45692
+ const token = ++linksSeq.current;
45693
+ setRecordId(id);
45694
+ setLinksLoading(true);
45695
+ setLinksError(null);
45696
+ setEntryPane(true);
45697
+ if (modeRef.current === "graph") setDetailOpen(true);
45698
+ rpc("dsh-memory/record-links", { recordId: id }).then((r) => {
45699
+ if (token !== linksSeq.current) return;
45700
+ if (!r.ok) throw new Error(r.error.message);
45701
+ const v = r.value;
45702
+ if (v.record) remember([v.record]);
45703
+ remember(v.records);
45704
+ setLinks(v);
45705
+ setEmphasisId(v.entities[0]?.id);
45706
+ if (!opts?.keepEgo && v.entities[0]) setEgoRoot(v.entities[0].id);
45707
+ }).catch((e) => {
45708
+ if (token !== linksSeq.current) return;
45709
+ setLinksError(String(e?.message ?? e));
45710
+ setLinks(null);
45711
+ }).finally(() => {
45712
+ if (token === linksSeq.current) setLinksLoading(false);
45713
+ });
45714
+ },
45715
+ [rpc, remember]
45716
+ );
45717
+ const onPickNode = (0, import_react33.useCallback)(
45718
+ (nodeId) => {
45719
+ setEmphasisId(nodeId);
45720
+ setDetailOpen(true);
45721
+ const known = links?.entities.find((e) => e.id === nodeId) ?? links?.neighbors.find((n) => n.id === nodeId);
45722
+ const first = known?.recordIds[0];
45723
+ if (first) {
45724
+ openRecord(first, { keepEgo: true });
45725
+ return;
45726
+ }
45727
+ rpc("dsh-memory/graph-node-get", { id: nodeId }).then((r) => {
45728
+ const node = r.ok ? r.value.node : null;
45729
+ const id = node?.sourceRecordIds?.[0];
45730
+ if (id) openRecord(id, { keepEgo: true });
45731
+ }).catch(() => {
45732
+ });
45733
+ },
45734
+ [links, openRecord, rpc]
45735
+ );
45736
+ const onPickNode2d = (0, import_react33.useCallback)(
45737
+ (nodeId) => {
45738
+ setEgoRoot(nodeId);
45739
+ onPickNode(nodeId);
45740
+ },
45741
+ [onPickNode]
45742
+ );
45743
+ const graphData = (0, import_react33.useMemo)(
45744
+ () => full && egoRoot ? egoSubgraph2(full, egoRoot) : full,
45745
+ [full, egoRoot]
45746
+ );
45747
+ const locateNode = (0, import_react33.useCallback)(
45748
+ (nodeId) => {
45749
+ setEmphasisId(nodeId);
45750
+ if (view === "2d") setEgoRoot(nodeId);
45751
+ setSearchOpen(false);
45752
+ },
45753
+ [view]
45754
+ );
45755
+ const searchHits = (0, import_react33.useMemo)(() => {
45756
+ const q = searchText.trim().toLowerCase();
45757
+ if (!q || !full) return [];
45758
+ const hits = full.nodes.filter((n) => n.name.toLowerCase().includes(q));
45759
+ return hits.slice(0, 200);
45760
+ }, [searchText, full]);
45761
+ const locateFirstHit = (0, import_react33.useCallback)(() => {
45762
+ const q = searchText.trim().toLowerCase();
45763
+ if (!q || !full) return;
45764
+ const hit = full.nodes.find((n) => n.name.toLowerCase().includes(q));
45765
+ if (!hit) {
45766
+ setSearchMiss(true);
45767
+ setTimeout(() => setSearchMiss(false), 1500);
45768
+ return;
45769
+ }
45770
+ setSearchMiss(false);
45771
+ locateNode(hit.id);
45772
+ }, [searchText, full, locateNode]);
45773
+ const jumpToGraph = (0, import_react33.useCallback)(
45774
+ (target) => {
45775
+ const root = links?.entities[0]?.id;
45776
+ setView(target);
45777
+ setMode("graph");
45778
+ setDetailOpen(true);
45779
+ if (root) {
45780
+ setEmphasisId(root);
45781
+ if (target === "2d") setEgoRoot(root);
45782
+ }
45783
+ },
45784
+ [links]
45785
+ );
45786
+ const backToEntry = (0, import_react33.useCallback)(() => {
45787
+ setMode("query");
45788
+ setEntryPane(true);
45789
+ }, []);
45790
+ const labelOfRecord = (0, import_react33.useCallback)((id) => labels.current.get(id) ?? id.slice(0, 10), []);
45791
+ const building = stats && stats.jobsTotal > 0 && stats.pendingJobs > 0;
45792
+ const summary = (0, import_react33.useMemo)(() => {
45793
+ if (!stats) return "统计未到";
45794
+ const drawn = graphData ? graphData.nodes.length : 0;
45795
+ const egoNote = egoRoot ? ` · ego ≤3 跳(${drawn} 节点)` : "";
45796
+ const cut = graphData?.truncated ? "(已截断,仅画部分)" : "";
45797
+ return `节点 ${drawn}/${stats.nodes} · 边 ${graphData ? graphData.edges.length : 0}/${stats.edges} · 因果边 ${stats.causalEdges}${cut}${egoNote}`;
45798
+ }, [stats, graphData, egoRoot]);
45799
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { ref: containerRef, style: { display: "flex", flexDirection: "column", gap: 8, padding: 12, height: "calc(100vh - 48px)", minHeight: 0 }, children: [
45800
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }, children: [
45801
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("h2", { style: { margin: 0, fontSize: 16 }, children: "记忆库" }),
45802
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: summary }),
45803
+ building ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { style: { fontSize: 12, color: "var(--dsh-mem-accent)" }, title: "图谱投影进行中", children: [
45804
+ "构建中 ",
45805
+ stats ? stats.jobsTotal - stats.pendingJobs : 0,
45806
+ "/",
45807
+ stats?.jobsTotal ?? 0
45808
+ ] }) : null,
45809
+ view === "3d" && full && full.nodes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { style: { display: "inline-flex", gap: 8, alignItems: "center", fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: [
45810
+ LEGEND_TYPES.filter(([t]) => full.nodes.some((n) => n.type === t)).map(([t, zh, token]) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { children: [
45811
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45812
+ "span",
45813
+ {
45814
+ style: { width: 8, height: 8, borderRadius: 8, display: "inline-block", marginRight: 3, background: `var(${token})` }
45815
+ }
45816
+ ),
45817
+ zh
45818
+ ] }, t)),
45819
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { title: "亮度=老化权重(来源记录新鲜度),拉近后依然分得出新旧", children: "亮度=老化" })
45820
+ ] }) : null,
45821
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { style: { flex: 1 } }),
45822
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { style: { display: "inline-flex", border: "1px solid var(--dsh-mem-border)", borderRadius: 6, overflow: "hidden", fontSize: 12 }, children: [
45823
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45824
+ "span",
45825
+ {
45826
+ onClick: () => setMode("query"),
45827
+ style: { padding: "3px 12px", cursor: "pointer", background: mode === "query" ? "var(--dsh-mem-accent)" : "transparent", color: mode === "query" ? "#fff" : "var(--dsh-mem-text-2)" },
45828
+ children: "查询"
45829
+ }
45830
+ ),
45831
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45832
+ "span",
45833
+ {
45834
+ onClick: () => setMode("graph"),
45835
+ style: { padding: "3px 12px", cursor: "pointer", background: mode === "graph" ? "var(--dsh-mem-accent)" : "transparent", color: mode === "graph" ? "#fff" : "var(--dsh-mem-text-2)" },
45836
+ children: "图"
45837
+ }
45838
+ )
45839
+ ] }),
45840
+ mode === "graph" ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
45841
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { style: { position: "relative", display: "inline-flex" }, children: [
45842
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45843
+ NInput,
45844
+ {
45845
+ value: searchText,
45846
+ onChange: (e) => {
45847
+ setSearchText(e.target.value);
45848
+ setSearchOpen(true);
45849
+ },
45850
+ onFocus: () => setSearchOpen(true),
45851
+ onBlur: () => setTimeout(() => setSearchOpen(false), 180),
45852
+ onKeyDown: (e) => {
45853
+ if (e.key === "Enter") locateFirstHit();
45854
+ if (e.key === "Escape") setSearchOpen(false);
45855
+ },
45856
+ placeholder: searchMiss ? "未命中该实体" : "搜实体名…",
45857
+ style: { width: 160, fontSize: 12 }
45858
+ }
45859
+ ),
45860
+ searchOpen && searchText.trim() && searchHits.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45861
+ "div",
45862
+ {
45863
+ style: {
45864
+ position: "absolute",
45865
+ top: "calc(100% + 4px)",
45866
+ left: 0,
45867
+ zIndex: 20,
45868
+ width: 240,
45869
+ maxHeight: 260,
45870
+ overflow: "auto",
45871
+ background: "var(--dsh-mem-bg-card)",
45872
+ border: "1px solid var(--dsh-mem-border)",
45873
+ borderRadius: 8,
45874
+ boxShadow: "0 6px 18px rgba(0,0,0,.25)"
45875
+ },
45876
+ children: searchHits.map((n) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
45877
+ "div",
45878
+ {
45879
+ onMouseDown: (e) => e.preventDefault(),
45880
+ onClick: () => locateNode(n.id),
45881
+ style: { padding: "5px 10px", fontSize: 12, cursor: "pointer", display: "flex", gap: 6, alignItems: "center" },
45882
+ title: `${n.type}${n.aging != null ? ` · 老化 ${n.aging.toFixed(2)}` : ""}`,
45883
+ children: [
45884
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: n.name }),
45885
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { style: { fontSize: 10, color: "var(--dsh-mem-text-3)" }, children: n.type })
45886
+ ]
45887
+ },
45888
+ n.id
45889
+ ))
45890
+ }
45891
+ ) : null,
45892
+ searchOpen && searchText.trim() && searchHits.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45893
+ "div",
45894
+ {
45895
+ style: {
45896
+ position: "absolute",
45897
+ top: "calc(100% + 4px)",
45898
+ left: 0,
45899
+ zIndex: 20,
45900
+ width: 240,
45901
+ padding: "6px 10px",
45902
+ fontSize: 12,
45903
+ background: "var(--dsh-mem-bg-card)",
45904
+ border: "1px solid var(--dsh-mem-border)",
45905
+ borderRadius: 8,
45906
+ color: "var(--dsh-mem-text-3)"
45907
+ },
45908
+ children: "图里没有匹配的实体"
45909
+ }
45910
+ ) : null
45911
+ ] }),
45912
+ view === "2d" && egoRoot ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45913
+ NButton,
45914
+ {
45915
+ onClick: () => setEgoRoot(null),
45916
+ title: "退出 ego 聚焦,重新渲染全图",
45917
+ style: { color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" },
45918
+ children: "回全图"
45919
+ }
45920
+ ) : null,
45921
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45922
+ NButton,
45923
+ {
45924
+ onClick: () => {
45925
+ if (!recordId && emphasisId) {
45926
+ onPickNode(emphasisId);
45927
+ }
45928
+ setDetailOpen((v) => !v);
45929
+ },
45930
+ title: "展开/收起右侧详情",
45931
+ style: detailOpen ? { color: "var(--dsh-mem-accent)", borderColor: "var(--dsh-mem-accent)" } : void 0,
45932
+ children: "详情"
45933
+ }
45934
+ ),
45935
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { style: { display: "inline-flex", border: "1px solid var(--dsh-mem-border)", borderRadius: 6, overflow: "hidden", fontSize: 12 }, children: [
45936
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45937
+ "span",
45938
+ {
45939
+ onClick: () => setView("2d"),
45940
+ title: "G6 力导星图",
45941
+ style: { padding: "3px 10px", cursor: "pointer", background: view === "2d" ? "var(--dsh-mem-accent)" : "transparent", color: view === "2d" ? "#fff" : "var(--dsh-mem-text-2)" },
45942
+ children: "2D"
45943
+ }
45944
+ ),
45945
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45946
+ "span",
45947
+ {
45948
+ onClick: () => setView("3d"),
45949
+ title: "Canvas 三维投影记忆海",
45950
+ style: { padding: "3px 10px", cursor: "pointer", background: view === "3d" ? "var(--dsh-mem-accent)" : "transparent", color: view === "3d" ? "#fff" : "var(--dsh-mem-text-2)" },
45951
+ children: "3D"
45952
+ }
45953
+ )
45954
+ ] })
45955
+ ] }) : null,
45956
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(NButton, { onClick: loadGraph, title: "重新拉取图与统计", children: "刷新图" }),
45957
+ onClose ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(NButton, { onClick: onClose, title: "关闭", children: "关闭" }) : null
45958
+ ] }),
45959
+ mode === "query" ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { style: { display: "flex", flex: 1, minHeight: 0, gap: 0 }, children: [
45960
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { style: { flex: 1, minWidth: 0, minHeight: 0, overflow: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(RecordPane, { rpc, activeId: recordId, onPick: openRecord }) }),
45961
+ entryPane && recordId ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
45962
+ "div",
45963
+ {
45964
+ style: {
45965
+ width: 420,
45966
+ minWidth: DETAIL_MIN,
45967
+ flexShrink: 0,
45968
+ minHeight: 0,
45969
+ overflow: "auto",
45970
+ borderLeft: "1px solid var(--dsh-mem-border)",
45971
+ paddingLeft: 10,
45972
+ display: "flex",
45973
+ flexDirection: "column",
45974
+ gap: 8
45975
+ },
45976
+ children: [
45977
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }, children: [
45978
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "在图中定位本条目的实体:" }),
45979
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(NButton, { onClick: () => jumpToGraph("2d"), disabled: !links || links.entities.length === 0, title: "切到 2D 力导星图并 ego 聚焦首实体", children: "2D 图" }),
45980
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(NButton, { onClick: () => jumpToGraph("3d"), disabled: !links || links.entities.length === 0, title: "切到 3D 记忆海并 ego 聚焦首实体", children: "3D 图" }),
45981
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { style: { flex: 1 } }),
45982
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(NButton, { onClick: () => setEntryPane(false), title: "收起条目面板", children: "收起" })
45983
+ ] }),
45984
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
45985
+ LinkPane,
45986
+ {
45987
+ recordId,
45988
+ links,
45989
+ loading: linksLoading,
45990
+ error: linksError,
45991
+ onPickRecord: openRecord,
45992
+ onEmphasisNode: setEmphasisId,
45993
+ labelOfRecord
45994
+ }
45995
+ )
45996
+ ]
45997
+ }
45998
+ ) : null
45999
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { style: { display: "flex", flex: 1, minHeight: 0, gap: 0 }, children: [
46000
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" }, children: graphError ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { style: { fontSize: 12, color: "var(--dsh-mem-accent)", padding: 8 }, children: [
46001
+ "图数据读取失败:",
46002
+ graphError,
46003
+ " ",
46004
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(NButton, { onClick: loadGraph, children: "重试" })
46005
+ ] }) : graphLoading ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)", padding: 8 }, children: "图加载中…" }) : full && full.nodes.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)", padding: 8 }, children: "图里还没有节点。图谱投影默认关闭——在设置页「概览 → 知识图谱」打开开关后,存量记忆会逐批投影成实体与关系。" }) : full ? view === "3d" ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Scene3D, { full, onPick: onPickNode, emphasisId, height: canvasHeight }) : /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(GraphCanvas, { full: graphData ?? full, onPick: onPickNode2d, emphasisId, fit: "view", height: "fill" }) : null }),
46006
+ detailOpen ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(import_jsx_runtime33.Fragment, { children: [
46007
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
46008
+ "div",
46009
+ {
46010
+ onMouseDown: startDrag,
46011
+ style: { width: 5, cursor: "col-resize", flexShrink: 0, background: "var(--dsh-mem-border)", borderRadius: 2, margin: "0 2px", alignSelf: "stretch" },
46012
+ title: "拖动调整详情宽度"
46013
+ }
46014
+ ),
46015
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { style: { width: detailW, minWidth: DETAIL_MIN, flexShrink: 0, overflow: "auto", minHeight: 0, borderLeft: "1px solid var(--dsh-mem-border)", paddingLeft: 10, display: "flex", flexDirection: "column", gap: 8 }, children: [
46016
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }, children: [
46017
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "本条目:" }),
46018
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(NButton, { onClick: backToEntry, disabled: !recordId, title: "切回查询视图并展开本条目(保留当前选点)", children: "按条目展示" })
46019
+ ] }),
46020
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
46021
+ LinkPane,
46022
+ {
46023
+ recordId,
46024
+ links,
46025
+ loading: linksLoading,
46026
+ error: linksError,
46027
+ onPickRecord: openRecord,
46028
+ onEmphasisNode: setEmphasisId,
46029
+ labelOfRecord
46030
+ }
46031
+ )
46032
+ ] })
46033
+ ] }) : null
46034
+ ] })
46035
+ ] });
46036
+ }
46037
+
46038
+ // client/src/explorer/ExplorerEntry.tsx
46039
+ var import_react34 = require("react");
46040
+ var import_jsx_runtime34 = require("react/jsx-runtime");
46041
+ function ExplorerIcon(props) {
46042
+ const size = props.size ?? 16;
46043
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
46044
+ "svg",
46045
+ {
46046
+ width: size,
46047
+ height: size,
46048
+ viewBox: "0 0 16 16",
46049
+ fill: "none",
46050
+ stroke: "currentColor",
46051
+ strokeWidth: 1.3,
46052
+ opacity: props.active ? 1 : 0.72,
46053
+ "aria-hidden": "true",
46054
+ children: [
46055
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("path", { d: "M4 11.5 L8 4 L12.5 9", strokeLinecap: "round" }),
46056
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("circle", { cx: "4", cy: "11.5", r: "1.7" }),
46057
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("circle", { cx: "8", cy: "4", r: "1.7" }),
46058
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("circle", { cx: "12.5", cy: "9", r: "1.7" })
46059
+ ]
46060
+ }
46061
+ );
46062
+ }
46063
+ function ExplorerFooterEntry(props) {
46064
+ const [open, setOpen] = (0, import_react34.useState)(false);
46065
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
46066
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("button", { className: "dsh-mem-btn", onClick: () => setOpen(true), title: "打开记忆库(查询 L1 记忆与关联图谱)", children: "记忆库" }),
46067
+ open ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(NModal, { title: "记忆库", onClose: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { style: { height: "70vh", minHeight: 420 }, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ExplorerPage, { rpc: props.rpc, canvasHeight: 360 }) }) }) : null
46068
+ ] });
46069
+ }
46070
+
44417
46071
  // client/src/entry.tsx
44418
46072
  var inject = ["slots"];
44419
46073
  var SETTINGS_SEAT = "settings.section";
44420
46074
  var SEAT_PROBE_MS = 3e3;
46075
+ var EXPLORER_ID = "dsh-memory-explorer";
46076
+ var PANEL_SEAT = "sidebar.panellist";
46077
+ var MAIN_SEAT = "main";
46078
+ var FOOTER_SEAT = "sidebar.footer.action";
46079
+ var GEN_017_SEAT = "sidebar.workspaces.session.menu.item";
46080
+ var EXPLORER_ORDER = 7;
44421
46081
  function apply(ctx) {
44422
46082
  const rpc = makeRpc(ctx);
44423
46083
  console.info("[dsh-prime-memory] client apply: slots 注入就绪,注册 UI 槽位");
@@ -44441,11 +46101,103 @@ var __create = Object.create;
44441
46101
  console.warn(`[dsh-prime-memory] ${SETTINGS_SEAT2} 槽位未声明:`, err);
44442
46102
  }
44443
46103
  setTimeout(() => {
44444
- if (cardSeatLive) return;
44445
- console.warn(
44446
- `[dsh-prime-memory] 宿主未声明 ${SETTINGS_SEAT2}:设置里的「记忆」面板不会出现。(0.1.5 的座位集合本机未验证,请在真机上确认。)`
44447
- );
46104
+ if (!cardSeatLive) {
46105
+ console.warn(
46106
+ `[dsh-prime-memory] 宿主未声明 ${SETTINGS_SEAT2}:设置里的「记忆」面板不会出现。(0.1.5 的座位集合本机未验证,请在真机上确认。)`
46107
+ );
46108
+ }
46109
+ if (!explorerMounted) {
46110
+ console.warn("[dsh-prime-memory] 记忆库入口未挂载:panellist/main/footer 三个座位都没等到。");
46111
+ }
44448
46112
  }, SEAT_PROBE_MS);
46113
+ let gen017 = false;
46114
+ let mainSeat = false;
46115
+ let wired = false;
46116
+ let explorerMounted = false;
46117
+ let mountedPage = false;
46118
+ let detach = null;
46119
+ const selectPanel = (id) => {
46120
+ try {
46121
+ const layout = ctx.get("layout");
46122
+ layout?.selectPanel?.(id);
46123
+ } catch {
46124
+ }
46125
+ };
46126
+ const mountPage = () => {
46127
+ const row2 = ctx.slots.inject(
46128
+ PANEL_SEAT,
46129
+ () => ctx.slots.register({ name: PANEL_SEAT, id: EXPLORER_ID, order: EXPLORER_ORDER, label: "记忆库" }, ExplorerIcon)
46130
+ );
46131
+ const page = ctx.slots.inject(
46132
+ MAIN_SEAT,
46133
+ () => ctx.slots.register(
46134
+ { name: MAIN_SEAT, key: EXPLORER_ID, inject: () => ({ rpc, onClose: () => selectPanel(null) }) },
46135
+ ExplorerPage
46136
+ )
46137
+ );
46138
+ return () => {
46139
+ page();
46140
+ row2();
46141
+ };
46142
+ };
46143
+ const mountFooter = () => ctx.slots.inject(
46144
+ FOOTER_SEAT,
46145
+ () => ctx.slots.register(
46146
+ { name: FOOTER_SEAT, id: EXPLORER_ID, order: EXPLORER_ORDER, inject: () => ({ rpc }) },
46147
+ ExplorerFooterEntry
46148
+ )
46149
+ );
46150
+ const sync = () => {
46151
+ if (!explorerEntryState.isConfirmed()) return;
46152
+ if (!explorerEntryState.isWanted()) {
46153
+ detach?.();
46154
+ detach = null;
46155
+ mountedPage = false;
46156
+ explorerMounted = true;
46157
+ return;
46158
+ }
46159
+ const wantPage = gen017 && mainSeat;
46160
+ if (detach !== null && wantPage === mountedPage) return;
46161
+ detach?.();
46162
+ try {
46163
+ detach = wantPage ? mountPage() : mountFooter();
46164
+ mountedPage = wantPage;
46165
+ explorerMounted = true;
46166
+ } catch (err) {
46167
+ console.warn("[dsh-prime-memory] 记忆库入口挂载失败:", err);
46168
+ detach = null;
46169
+ }
46170
+ };
46171
+ explorerEntryState.subscribe(() => sync());
46172
+ try {
46173
+ ctx.slots.inject(GEN_017_SEAT, () => {
46174
+ gen017 = true;
46175
+ if (wired) sync();
46176
+ return () => {
46177
+ gen017 = false;
46178
+ if (wired) sync();
46179
+ };
46180
+ });
46181
+ ctx.slots.inject(MAIN_SEAT, () => {
46182
+ mainSeat = true;
46183
+ if (wired) sync();
46184
+ return () => {
46185
+ mainSeat = false;
46186
+ if (wired) sync();
46187
+ };
46188
+ });
46189
+ } catch (err) {
46190
+ console.warn("[dsh-prime-memory] 记忆库座位探针失败(降级为不挂载):", err);
46191
+ }
46192
+ wired = true;
46193
+ void rpc("dsh-memory/settings-get", {}).then((res) => {
46194
+ explorerEntryState.confirm(
46195
+ res.ok && res.value.supported ? res.value.settings.explorerEnabled !== false : true
46196
+ // settings 服务缺席 → fail-open,与旧行为一致
46197
+ );
46198
+ }).catch(() => {
46199
+ explorerEntryState.confirm(true);
46200
+ });
44449
46201
  try {
44450
46202
  ctx.slots.inject("conversation.input.left", () => {
44451
46203
  return ctx.slots.register(