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 +1760 -8
- package/dist/config.js +6 -0
- package/dist/contract.d.ts +66 -1
- package/dist/graph/constraints.d.ts +14 -0
- package/dist/graph/constraints.js +14 -0
- package/dist/graph/preview.d.ts +22 -0
- package/dist/graph/preview.js +25 -7
- package/dist/graph/record-links.d.ts +61 -0
- package/dist/graph/record-links.js +167 -0
- package/dist/graph/select.d.ts +30 -0
- package/dist/graph/select.js +124 -0
- package/dist/settings.js +3 -0
- package/dist/stats.d.ts +3 -2
- package/dist/stats.js +81 -5
- package/dist/store/graph-store.d.ts +37 -1
- package/dist/store/graph-store.js +120 -14
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
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 ||
|
|
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) =>
|
|
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,
|
|
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:
|
|
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)
|
|
44445
|
-
|
|
44446
|
-
|
|
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(
|