@modusensus/dsh-mneme 0.7.10 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +437 -0
- package/README.md +364 -166
- package/bin/cli.mjs +603 -0
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/lib/api-standalone.js +264 -0
- package/{dsh-mneme/lib → lib}/api.js +133 -289
- package/{dsh-mneme/lib → lib}/client.js +1008 -922
- package/{dsh-mneme/lib → lib}/config.js +54 -99
- package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
- package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
- package/{dsh-mneme/lib → lib}/dream.js +22 -168
- package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
- package/{dsh-mneme/lib → lib}/index.js +45 -42
- package/{dsh-mneme/lib → lib}/inject.js +11 -53
- package/{dsh-mneme/src → lib}/mirror.js +1 -12
- package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
- package/{dsh-mneme/lib → lib}/reranker.js +1 -4
- package/{dsh-mneme/lib → lib}/service.js +20 -410
- package/{dsh-mneme/src → lib}/settings.js +34 -64
- package/{dsh-mneme/lib → lib}/store.js +38 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +45 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/src/api-standalone.js +264 -0
- package/{dsh-mneme/src → src}/api.js +133 -289
- package/{dsh-mneme/src → src}/config.js +54 -99
- package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
- package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
- package/{dsh-mneme/src → src}/dream.js +22 -168
- package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
- package/{dsh-mneme/src → src}/index.js +45 -42
- package/{dsh-mneme/src → src}/inject.js +11 -53
- package/{dsh-mneme/lib → src}/mirror.js +1 -12
- package/{dsh-mneme/src → src}/quality-filter.js +1 -3
- package/{dsh-mneme/src → src}/reranker.js +1 -4
- package/{dsh-mneme/src → src}/service.js +20 -410
- package/{dsh-mneme/lib → src}/settings.js +34 -64
- package/{dsh-mneme/src → src}/store.js +38 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +45 -156
- package/{dsh-mneme/test → test}/client.test.js +10 -206
- package/{dsh-mneme/test → test}/config.test.js +0 -21
- package/{dsh-mneme/test → test}/dream.test.js +1 -160
- package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
- package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
- package/test/inject.test.js +120 -0
- package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
- package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
- package/{dsh-mneme/test → test}/reranker.test.js +0 -43
- package/{dsh-mneme/test → test}/service-search.test.js +0 -25
- package/{dsh-mneme/test → test}/service.test.js +0 -106
- package/{dsh-mneme/test → test}/settings.test.js +16 -35
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- package/test/standalone-api.test.js +326 -0
- package/{dsh-mneme/test → test}/store.test.js +0 -76
- package/{dsh-mneme/test → test}/summarize.test.js +16 -15
- package/test/tools.test.js +265 -0
- package/.github/workflows/publish.yml +0 -58
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -91
- package/CONTRIBUTING.md +0 -293
- package/SECURITY.md +0 -718
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -419
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -501
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
- package/dsh-mneme/lib/heat.js +0 -136
- package/dsh-mneme/lib/parser/tag.js +0 -59
- package/dsh-mneme/lib/parser/wiki-link.js +0 -38
- package/dsh-mneme/lib/search/tag-boost.js +0 -61
- package/dsh-mneme/lib/tools.js +0 -458
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -83
- package/dsh-mneme/scripts/check-sync.js +0 -42
- package/dsh-mneme/src/client.js +0 -2242
- package/dsh-mneme/src/dream/tag-extractor.js +0 -156
- package/dsh-mneme/src/heat.js +0 -136
- package/dsh-mneme/src/parser/tag.js +0 -59
- package/dsh-mneme/src/parser/wiki-link.js +0 -38
- package/dsh-mneme/src/search/tag-boost.js +0 -61
- package/dsh-mneme/src/tools.js +0 -458
- package/dsh-mneme/test/boundary-v0625.test.js +0 -82
- package/dsh-mneme/test/directory.test.js +0 -134
- package/dsh-mneme/test/heat.test.js +0 -148
- package/dsh-mneme/test/inject.test.js +0 -206
- package/dsh-mneme/test/layered-types-stats.test.js +0 -144
- package/dsh-mneme/test/lib-smoke.test.js +0 -109
- package/dsh-mneme/test/normalize-decisions.test.js +0 -120
- package/dsh-mneme/test/provenance.test.js +0 -103
- package/dsh-mneme/test/recall-runs.test.js +0 -93
- package/dsh-mneme/test/sleep-heat.test.js +0 -112
- package/dsh-mneme/test/tag-boost.test.js +0 -125
- package/dsh-mneme/test/tag.test.js +0 -426
- package/dsh-mneme/test/tools.test.js +0 -674
- package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
- package/dsh-mneme/test/wiki-link.test.js +0 -332
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
|
@@ -39,6 +39,41 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
h("circle", { cx: 12.4, cy: 12, r: 1.8, fill: "currentColor" })
|
|
40
40
|
);
|
|
41
41
|
|
|
42
|
+
// Stroke icons, Lucide path data (ISC license) inlined as [tag, attrs]
|
|
43
|
+
// tuples — the plugin runtime cannot require third-party libraries, so
|
|
44
|
+
// the data ships with the bundle (the morphicons/lucide pairing the
|
|
45
|
+
// data package documents, minus the runtime dependency). Stroke picks
|
|
46
|
+
// up currentColor, so icons follow the host theme tokens.
|
|
47
|
+
const ICON_PATHS = {
|
|
48
|
+
database: [["ellipse", { cx: "12", cy: "5", rx: "9", ry: "3" }], ["path", { d: "M3 5V19A9 3 0 0 0 21 19V5" }], ["path", { d: "M3 12A9 3 0 0 0 21 12" }]],
|
|
49
|
+
waypoints: [["path", { d: "m10.586 5.414-5.172 5.172" }], ["path", { d: "m18.586 13.414-5.172 5.172" }], ["path", { d: "M6 12h12" }], ["circle", { cx: "12", cy: "20", r: "2" }], ["circle", { cx: "12", cy: "4", r: "2" }], ["circle", { cx: "20", cy: "12", r: "2" }], ["circle", { cx: "4", cy: "12", r: "2" }]],
|
|
50
|
+
settings: [["path", { d: "M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" }], ["circle", { cx: "12", cy: "12", r: "3" }]],
|
|
51
|
+
search: [["path", { d: "m21 21-4.34-4.34" }], ["circle", { cx: "11", cy: "11", r: "8" }]],
|
|
52
|
+
refresh: [["path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }], ["path", { d: "M21 3v5h-5" }], ["path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }], ["path", { d: "M8 16H3v5" }]],
|
|
53
|
+
chevronDown: [["path", { d: "m6 9 6 6 6-6" }]],
|
|
54
|
+
chevronRight: [["path", { d: "m9 18 6-6-6-6" }]],
|
|
55
|
+
copy: [["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }], ["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" }]],
|
|
56
|
+
check: [["path", { d: "M20 6 9 17l-5-5" }]],
|
|
57
|
+
inbox: [["polyline", { points: "22 12 16 12 14 15 10 15 8 12 2 12" }], ["path", { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" }]],
|
|
58
|
+
activity: [["path", { d: "M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" }]]
|
|
59
|
+
};
|
|
60
|
+
const Icon = ({ name, size = 16, className }) => {
|
|
61
|
+
const parts = ICON_PATHS[name];
|
|
62
|
+
if (!parts) return null;
|
|
63
|
+
return h("svg", {
|
|
64
|
+
width: size,
|
|
65
|
+
height: size,
|
|
66
|
+
viewBox: "0 0 24 24",
|
|
67
|
+
fill: "none",
|
|
68
|
+
stroke: "currentColor",
|
|
69
|
+
strokeWidth: 2,
|
|
70
|
+
strokeLinecap: "round",
|
|
71
|
+
strokeLinejoin: "round",
|
|
72
|
+
className,
|
|
73
|
+
"aria-hidden": "true"
|
|
74
|
+
}, parts.map(([tag, attrs], i) => h(tag, { key: i, ...attrs })));
|
|
75
|
+
};
|
|
76
|
+
|
|
42
77
|
// Unified API fetcher: attaches the optional apiToken (set in the settings
|
|
43
78
|
// view, persisted in localStorage) as a Bearer header. When no token has
|
|
44
79
|
// been configured the header is omitted and the API stays open (default).
|
|
@@ -65,8 +100,6 @@ window.__ModuleLoader__.load({
|
|
|
65
100
|
"memory.tab.project": "项目",
|
|
66
101
|
"memory.tab.decision": "决策",
|
|
67
102
|
"memory.tab.history": "历史",
|
|
68
|
-
"memory.tab.user": "用户",
|
|
69
|
-
"memory.tab.fact": "事实",
|
|
70
103
|
"memory.settings.title": "记忆库设置",
|
|
71
104
|
"memory.settings.profile": "用户画像",
|
|
72
105
|
"memory.settings.profileHint": "写一段自我介绍(角色、背景、偏好),Agent 每轮对话都会自动带上。",
|
|
@@ -74,16 +107,6 @@ window.__ModuleLoader__.load({
|
|
|
74
107
|
"memory.settings.profileSaved": "画像已保存",
|
|
75
108
|
"memory.settings.rules": "规则",
|
|
76
109
|
"memory.settings.rulesHint": "给 Agent 立下必须遵守的规矩,随时增删,下一轮即生效。",
|
|
77
|
-
"memory.settings.autoTagTitle": "自动打标签",
|
|
78
|
-
"memory.settings.autoTagHint": "开启后,新记忆由轻量模型自动打标签,目录页按标签自动分类(opt-in,默认关)",
|
|
79
|
-
"memory.settings.autoTagEnabled": "自动打标签",
|
|
80
|
-
"memory.settings.autoTagSave": "保存",
|
|
81
|
-
"memory.settings.autoTagSaved": "已保存",
|
|
82
|
-
"memory.settings.sidebarTriggerTitle": "侧边栏入口按钮",
|
|
83
|
-
"memory.settings.sidebarTriggerHint": "关闭后隐藏侧边栏底部(左下角)的记忆入口按钮。与其他插件 UI 冲突时建议关闭;记忆库仍可通过顶部「记忆库」标签访问",
|
|
84
|
-
"memory.settings.sidebarTriggerEnabled": "显示左下角入口按钮",
|
|
85
|
-
"memory.settings.sidebarTriggerSave": "保存",
|
|
86
|
-
"memory.settings.sidebarTriggerSaved": "已保存",
|
|
87
110
|
"memory.settings.ruleAdd": "添加规则",
|
|
88
111
|
"memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
|
|
89
112
|
"memory.settings.commands": "自定义指令",
|
|
@@ -99,10 +122,23 @@ window.__ModuleLoader__.load({
|
|
|
99
122
|
"memory.view.label": "记忆库",
|
|
100
123
|
"memory.overlay.close": "关闭",
|
|
101
124
|
"memory.explorer.tabMemory": "记忆",
|
|
102
|
-
"memory.explorer.
|
|
103
|
-
"memory.explorer.tabDirectory": "目录",
|
|
125
|
+
"memory.explorer.tabEntities": "实体",
|
|
104
126
|
"memory.explorer.tabSettings": "设置",
|
|
105
|
-
"memory.explorer.
|
|
127
|
+
"memory.explorer.tabStatus": "状态",
|
|
128
|
+
"memory.entities.count": "{n} 个实体",
|
|
129
|
+
"memory.entities.pick": "从左侧选择一个实体,查看它的属性与关系",
|
|
130
|
+
"memory.entities.none": "暂无实体:开启实体抽取后,记忆里的人物 / 项目 / 概念会自动沉淀到这里",
|
|
131
|
+
"memory.entities.attrs": "属性",
|
|
132
|
+
"memory.entities.relations": "关系",
|
|
133
|
+
"memory.entities.mentions": "{n} 次提及",
|
|
134
|
+
"memory.entities.lastSeen": "最近出现",
|
|
135
|
+
"memory.entities.graph": "关系图谱",
|
|
136
|
+
"memory.entity.person": "人物",
|
|
137
|
+
"memory.entity.project": "项目",
|
|
138
|
+
"memory.entity.concept": "概念",
|
|
139
|
+
"memory.entity.technology": "技术",
|
|
140
|
+
"memory.entity.organization": "组织",
|
|
141
|
+
"memory.entity.other": "其他",
|
|
106
142
|
"memory.explorer.search": "搜索标题或内容…",
|
|
107
143
|
"memory.explorer.searchTitle": "语义检索",
|
|
108
144
|
"memory.explorer.types": "分类",
|
|
@@ -112,34 +148,16 @@ window.__ModuleLoader__.load({
|
|
|
112
148
|
"memory.explorer.copy": "复制全文",
|
|
113
149
|
"memory.explorer.copied": "已复制",
|
|
114
150
|
"memory.explorer.refresh": "刷新",
|
|
151
|
+
"memory.explorer.loadMore": "加载更多",
|
|
115
152
|
"memory.explorer.count": "共 {n} 条",
|
|
116
153
|
"memory.explorer.empty": "暂无记忆条目",
|
|
117
154
|
"memory.explorer.source": "来源",
|
|
118
155
|
"memory.explorer.created": "创建",
|
|
119
156
|
"memory.explorer.updated": "更新",
|
|
120
157
|
"memory.explorer.tags": "标签",
|
|
121
|
-
"memory.explorer.tagAdd": "添加标签",
|
|
122
|
-
"memory.explorer.tagRemove": "移除标签",
|
|
123
|
-
"memory.explorer.tagPlaceholder": "输入标签,回车添加",
|
|
124
|
-
"memory.explorer.tagsEmpty": "暂无标签",
|
|
125
158
|
"memory.explorer.importance": "重要性",
|
|
126
159
|
"memory.explorer.topK": "返回数量",
|
|
127
160
|
"memory.explorer.topKOption": "返回 {n} 条",
|
|
128
|
-
"memory.overview.empty": "还没有记忆。开始对话后 Agent 会自动沉淀记忆,这里会展示分层总览",
|
|
129
|
-
"memory.overview.profileEmpty": "暂无用户画像记忆,可在对话中让 AI 记录",
|
|
130
|
-
"memory.overview.distribution": "类型分布",
|
|
131
|
-
"memory.overview.layers": "记忆分层",
|
|
132
|
-
"memory.overview.trend": "近 7 天趋势",
|
|
133
|
-
"memory.overview.none": "暂无",
|
|
134
|
-
"memory.overview.badgeInject": "常注入",
|
|
135
|
-
"memory.overview.badgeImportance": "按重要性",
|
|
136
|
-
"memory.directory.untagged": "无标签",
|
|
137
|
-
"memory.directory.loading": "加载中…",
|
|
138
|
-
"memory.directory.empty": "暂无记忆条目",
|
|
139
|
-
"directory.editToggle": "编辑模式(显示删除)",
|
|
140
|
-
"directory.deleteConfirm": "确定删除这条记忆吗?",
|
|
141
|
-
"directory.deleteConfirmShort": "确认?",
|
|
142
|
-
"directory.deleteError": "删除失败",
|
|
143
161
|
"memory.card.open": "在主区记忆库中查看全文",
|
|
144
162
|
"memory.time.now": "刚刚",
|
|
145
163
|
"memory.time.seconds": "{n}秒前",
|
|
@@ -158,7 +176,7 @@ window.__ModuleLoader__.load({
|
|
|
158
176
|
"memory.graph.sourceMemory": "查看来源记忆",
|
|
159
177
|
"memory.graph.hint": "拖拽节点调整布局 · 空白处拖动平移 · 滚轮缩放",
|
|
160
178
|
"memory.graph.resetView": "重置视图",
|
|
161
|
-
"memory.graph.viewInGraph": "
|
|
179
|
+
"memory.graph.viewInGraph": "在实体视图中打开",
|
|
162
180
|
"memory.graph.loading": "加载中…",
|
|
163
181
|
"memory.graph.distance": "距中心 {n} 跳",
|
|
164
182
|
"memory.settings.vectorTitle": "向量搜索",
|
|
@@ -177,11 +195,36 @@ window.__ModuleLoader__.load({
|
|
|
177
195
|
"memory.settings.apiTokenPlaceholder": "留空 = 不鉴权(默认)",
|
|
178
196
|
"memory.settings.apiTokenSave": "保存 Token",
|
|
179
197
|
"memory.settings.apiTokenSaved": "Token 已保存",
|
|
180
|
-
"memory.
|
|
181
|
-
"memory.
|
|
182
|
-
"memory.
|
|
183
|
-
"memory.
|
|
184
|
-
"memory.
|
|
198
|
+
"memory.settings.mode.title": "运行模式",
|
|
199
|
+
"memory.settings.mode.desc": "轻量模式只保留核心的记忆读写与自动注入(关闭 autoDream 巩固、实体抽取、语义搜索等高级功能),适合只想「记住偏好」的轻量使用;标准模式开启全部功能。",
|
|
200
|
+
"memory.settings.mode.light": "轻量",
|
|
201
|
+
"memory.settings.mode.standard": "标准",
|
|
202
|
+
"memory.settings.mode.savedHint": "已保存,重启 DSH 后生效",
|
|
203
|
+
"memory.settings.mode.offList": "已关闭:巩固(autoDream)· 实体抽取 · 语义搜索",
|
|
204
|
+
"memory.settings.extapi.title": "外部访问 API",
|
|
205
|
+
"memory.settings.extapi.desc": "独立 HTTP 服务,供其他插件 / CLI / 桌面工具读写记忆,默认绑定 127.0.0.1。重启 DSH 后生效。",
|
|
206
|
+
"memory.settings.extapi.enabled": "启用",
|
|
207
|
+
"memory.settings.extapi.disabled": "停用",
|
|
208
|
+
"memory.settings.extapi.address": "地址",
|
|
209
|
+
"memory.settings.extapi.port": "端口",
|
|
210
|
+
"memory.settings.extapi.token": "Token",
|
|
211
|
+
"memory.settings.extapi.copy": "复制",
|
|
212
|
+
"memory.settings.extapi.copied": "已复制",
|
|
213
|
+
"memory.settings.extapi.savedHint": "已保存,重启 DSH 后生效",
|
|
214
|
+
"memory.settings.extapi.invalidPort": "端口需为 1-65535 的数字",
|
|
215
|
+
"memory.explorer.delete": "删除",
|
|
216
|
+
"memory.explorer.confirmDelete": "确认删除?",
|
|
217
|
+
"memory.explorer.cancel": "取消",
|
|
218
|
+
"memory.explorer.deleted": "已删除",
|
|
219
|
+
"memory.explorer.deleteFailed": "删除失败",
|
|
220
|
+
"memory.status.memories": "记忆总数",
|
|
221
|
+
"memory.status.entities": "实体",
|
|
222
|
+
"memory.status.vector": "向量索引",
|
|
223
|
+
"memory.status.vectorOn": "已启用",
|
|
224
|
+
"memory.status.vectorOff": "未启用",
|
|
225
|
+
"memory.status.llm": "LLM 消耗",
|
|
226
|
+
"memory.status.llmCalls": "近 7 天 · {n} 次调用",
|
|
227
|
+
"memory.status.error": "加载失败"
|
|
185
228
|
},
|
|
186
229
|
en: {
|
|
187
230
|
"memory.panel.empty": "No memories yet",
|
|
@@ -191,8 +234,6 @@ window.__ModuleLoader__.load({
|
|
|
191
234
|
"memory.tab.project": "Projects",
|
|
192
235
|
"memory.tab.decision": "Decisions",
|
|
193
236
|
"memory.tab.history": "History",
|
|
194
|
-
"memory.tab.user": "User",
|
|
195
|
-
"memory.tab.fact": "Facts",
|
|
196
237
|
"memory.settings.title": "Memory Settings",
|
|
197
238
|
"memory.settings.profile": "User Profile",
|
|
198
239
|
"memory.settings.profileHint": "Describe yourself once — the agent reads it every turn.",
|
|
@@ -200,16 +241,6 @@ window.__ModuleLoader__.load({
|
|
|
200
241
|
"memory.settings.profileSaved": "Profile saved",
|
|
201
242
|
"memory.settings.rules": "Rules",
|
|
202
243
|
"memory.settings.rulesHint": "Rules the agent must follow — add or remove anytime, effective next turn.",
|
|
203
|
-
"memory.settings.autoTagTitle": "Auto-Tag",
|
|
204
|
-
"memory.settings.autoTagHint": "When on, new memories are auto-tagged by a light model and the directory auto-grouped by tag (opt-in, off by default)",
|
|
205
|
-
"memory.settings.autoTagEnabled": "Auto-tagging",
|
|
206
|
-
"memory.settings.autoTagSave": "Save",
|
|
207
|
-
"memory.settings.autoTagSaved": "Saved",
|
|
208
|
-
"memory.settings.sidebarTriggerTitle": "Sidebar trigger",
|
|
209
|
-
"memory.settings.sidebarTriggerHint": "Hide the memory entrance button at the sidebar footer (bottom-left) when it clashes with other plugins. The library stays reachable via the 记忆库 conversation tab",
|
|
210
|
-
"memory.settings.sidebarTriggerEnabled": "Show the bottom-left trigger",
|
|
211
|
-
"memory.settings.sidebarTriggerSave": "Save",
|
|
212
|
-
"memory.settings.sidebarTriggerSaved": "Saved",
|
|
213
244
|
"memory.settings.ruleAdd": "Add Rule",
|
|
214
245
|
"memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
|
|
215
246
|
"memory.settings.commands": "Custom Commands",
|
|
@@ -225,10 +256,23 @@ window.__ModuleLoader__.load({
|
|
|
225
256
|
"memory.view.label": "Memory",
|
|
226
257
|
"memory.overlay.close": "Close",
|
|
227
258
|
"memory.explorer.tabMemory": "Memories",
|
|
228
|
-
"memory.explorer.
|
|
229
|
-
"memory.explorer.tabDirectory": "Directory",
|
|
259
|
+
"memory.explorer.tabEntities": "Entities",
|
|
230
260
|
"memory.explorer.tabSettings": "Settings",
|
|
231
|
-
"memory.explorer.
|
|
261
|
+
"memory.explorer.tabStatus": "Status",
|
|
262
|
+
"memory.entities.count": "{n} entities",
|
|
263
|
+
"memory.entities.pick": "Select an entity to see its attributes and relations",
|
|
264
|
+
"memory.entities.none": "No entities yet — turn on entity extraction and people / projects / concepts will accumulate here",
|
|
265
|
+
"memory.entities.attrs": "Attributes",
|
|
266
|
+
"memory.entities.relations": "Relations",
|
|
267
|
+
"memory.entities.mentions": "{n} mentions",
|
|
268
|
+
"memory.entities.lastSeen": "Last seen",
|
|
269
|
+
"memory.entities.graph": "Relation graph",
|
|
270
|
+
"memory.entity.person": "People",
|
|
271
|
+
"memory.entity.project": "Projects",
|
|
272
|
+
"memory.entity.concept": "Concepts",
|
|
273
|
+
"memory.entity.technology": "Technologies",
|
|
274
|
+
"memory.entity.organization": "Organizations",
|
|
275
|
+
"memory.entity.other": "Others",
|
|
232
276
|
"memory.explorer.search": "Search title or content…",
|
|
233
277
|
"memory.explorer.searchTitle": "Semantic Search",
|
|
234
278
|
"memory.explorer.types": "Types",
|
|
@@ -238,34 +282,16 @@ window.__ModuleLoader__.load({
|
|
|
238
282
|
"memory.explorer.copy": "Copy content",
|
|
239
283
|
"memory.explorer.copied": "Copied",
|
|
240
284
|
"memory.explorer.refresh": "Refresh",
|
|
285
|
+
"memory.explorer.loadMore": "Load more",
|
|
241
286
|
"memory.explorer.count": "{n} items",
|
|
242
287
|
"memory.explorer.empty": "No memories yet",
|
|
243
288
|
"memory.explorer.source": "Source",
|
|
244
289
|
"memory.explorer.created": "Created",
|
|
245
290
|
"memory.explorer.updated": "Updated",
|
|
246
291
|
"memory.explorer.tags": "Tags",
|
|
247
|
-
"memory.explorer.tagAdd": "Add tag",
|
|
248
|
-
"memory.explorer.tagRemove": "Remove tag",
|
|
249
|
-
"memory.explorer.tagPlaceholder": "Type a tag, press Enter",
|
|
250
|
-
"memory.explorer.tagsEmpty": "No tags",
|
|
251
292
|
"memory.explorer.importance": "Importance",
|
|
252
293
|
"memory.explorer.topK": "Results limit",
|
|
253
294
|
"memory.explorer.topKOption": "Return {n}",
|
|
254
|
-
"memory.overview.empty": "No memories yet. Once you chat, the agent starts saving memories and the layered overview appears here",
|
|
255
|
-
"memory.overview.profileEmpty": "No user-profile memories yet — ask the agent to remember things about you in chat",
|
|
256
|
-
"memory.overview.distribution": "By Type",
|
|
257
|
-
"memory.overview.layers": "Memory Layers",
|
|
258
|
-
"memory.overview.trend": "Last 7 days",
|
|
259
|
-
"memory.overview.none": "None",
|
|
260
|
-
"memory.overview.badgeInject": "Auto-inject",
|
|
261
|
-
"memory.overview.badgeImportance": "By importance",
|
|
262
|
-
"memory.directory.untagged": "Untagged",
|
|
263
|
-
"memory.directory.loading": "Loading…",
|
|
264
|
-
"memory.directory.empty": "No memories yet",
|
|
265
|
-
"directory.editToggle": "Edit mode",
|
|
266
|
-
"directory.deleteConfirm": "Delete this memory?",
|
|
267
|
-
"directory.deleteConfirmShort": "Confirm?",
|
|
268
|
-
"directory.deleteError": "Delete failed",
|
|
269
295
|
"memory.card.open": "Open full text in the Memory tab",
|
|
270
296
|
"memory.time.now": "just now",
|
|
271
297
|
"memory.time.seconds": "{n}s ago",
|
|
@@ -284,7 +310,7 @@ window.__ModuleLoader__.load({
|
|
|
284
310
|
"memory.graph.sourceMemory": "View source memory",
|
|
285
311
|
"memory.graph.hint": "Drag nodes to rearrange · drag the background to pan · scroll to zoom",
|
|
286
312
|
"memory.graph.resetView": "Reset view",
|
|
287
|
-
"memory.graph.viewInGraph": "
|
|
313
|
+
"memory.graph.viewInGraph": "Open in entity explorer",
|
|
288
314
|
"memory.graph.loading": "Loading…",
|
|
289
315
|
"memory.graph.distance": "{n} hop(s) from root",
|
|
290
316
|
"memory.settings.vectorTitle": "Vector Search",
|
|
@@ -303,11 +329,36 @@ window.__ModuleLoader__.load({
|
|
|
303
329
|
"memory.settings.apiTokenPlaceholder": "Empty = no auth (default)",
|
|
304
330
|
"memory.settings.apiTokenSave": "Save Token",
|
|
305
331
|
"memory.settings.apiTokenSaved": "Token saved",
|
|
306
|
-
"memory.
|
|
307
|
-
"memory.
|
|
308
|
-
"memory.
|
|
309
|
-
"memory.
|
|
310
|
-
"memory.
|
|
332
|
+
"memory.settings.mode.title": "Runtime mode",
|
|
333
|
+
"memory.settings.mode.desc": "Light mode keeps only the core memory read/write and auto-injection (autoDream consolidation, entity extraction and semantic search are off) — for light use where you just want preferences remembered. Standard mode enables everything.",
|
|
334
|
+
"memory.settings.mode.light": "Light",
|
|
335
|
+
"memory.settings.mode.standard": "Standard",
|
|
336
|
+
"memory.settings.mode.savedHint": "Saved. Takes effect after restarting DSH",
|
|
337
|
+
"memory.settings.mode.offList": "Off: consolidation (autoDream) · entity extraction · semantic search",
|
|
338
|
+
"memory.settings.extapi.title": "External API",
|
|
339
|
+
"memory.settings.extapi.desc": "A standalone HTTP service for other plugins / CLIs / desktop tools to read and write memories, bound to 127.0.0.1 by default. Takes effect after restarting DSH.",
|
|
340
|
+
"memory.settings.extapi.enabled": "Enable",
|
|
341
|
+
"memory.settings.extapi.disabled": "Disable",
|
|
342
|
+
"memory.settings.extapi.address": "Address",
|
|
343
|
+
"memory.settings.extapi.port": "Port",
|
|
344
|
+
"memory.settings.extapi.token": "Token",
|
|
345
|
+
"memory.settings.extapi.copy": "Copy",
|
|
346
|
+
"memory.settings.extapi.copied": "Copied",
|
|
347
|
+
"memory.settings.extapi.savedHint": "Saved. Takes effect after restarting DSH",
|
|
348
|
+
"memory.settings.extapi.invalidPort": "Port must be a number between 1 and 65535",
|
|
349
|
+
"memory.explorer.delete": "Delete",
|
|
350
|
+
"memory.explorer.confirmDelete": "Confirm delete?",
|
|
351
|
+
"memory.explorer.cancel": "Cancel",
|
|
352
|
+
"memory.explorer.deleted": "Deleted",
|
|
353
|
+
"memory.explorer.deleteFailed": "Delete failed",
|
|
354
|
+
"memory.status.memories": "Total memories",
|
|
355
|
+
"memory.status.entities": "Entities",
|
|
356
|
+
"memory.status.vector": "Vector index",
|
|
357
|
+
"memory.status.vectorOn": "Enabled",
|
|
358
|
+
"memory.status.vectorOff": "Disabled",
|
|
359
|
+
"memory.status.llm": "LLM Usage",
|
|
360
|
+
"memory.status.llmCalls": "Last 7 days · {n} calls",
|
|
361
|
+
"memory.status.error": "Failed to load"
|
|
311
362
|
}
|
|
312
363
|
};
|
|
313
364
|
|
|
@@ -375,17 +426,6 @@ window.__ModuleLoader__.load({
|
|
|
375
426
|
".mneme-hint{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
|
|
376
427
|
".mneme-entitychip{flex:none;height:26px;padding:0 10px;border-radius:8px;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
|
|
377
428
|
".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
378
|
-
// --- tag chips in the detail meta (v0.6.2) ---
|
|
379
|
-
".mneme-tagrow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
|
|
380
|
-
".mneme-tagchip{display:inline-flex;align-items:center;gap:4px;max-width:100%;height:22px;padding:0 4px 0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px}",
|
|
381
|
-
".mneme-tagchip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
382
|
-
".mneme-tagremove{flex:none;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border:none;border-radius:4px;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-family:inherit;font-size:12px;line-height:1;padding:0}",
|
|
383
|
-
".mneme-tagremove:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
384
|
-
".mneme-tagadd{border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:2px 8px;border-radius:6px;flex:none}",
|
|
385
|
-
".mneme-tagadd:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
386
|
-
".mneme-taginput{box-sizing:border-box;height:22px;padding:0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:12px;outline:none;width:150px}",
|
|
387
|
-
".mneme-taginput:focus{border-color:var(--dsw-alias-state-business-primary)}",
|
|
388
|
-
".mneme-tagempty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}",
|
|
389
429
|
// --- main-area memory library page ---
|
|
390
430
|
".mneme-x{flex:1;min-height:0;height:100%;width:100%;box-sizing:border-box;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}",
|
|
391
431
|
".mneme-xbar{flex:none;display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:0 16px}",
|
|
@@ -397,7 +437,11 @@ window.__ModuleLoader__.load({
|
|
|
397
437
|
".mneme-vtab.mneme-active::after{content:\"\";position:absolute;left:8px;right:8px;bottom:-1px;height:2px;border-radius:2px;background:var(--dsw-alias-state-business-primary)}",
|
|
398
438
|
".mneme-xtools{margin-left:auto;display:flex;align-items:center;gap:8px;padding:0 0 0 12px}",
|
|
399
439
|
".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
|
|
400
|
-
// ---
|
|
440
|
+
// --- three-column browse layout: hairline separators, no outer box ---
|
|
441
|
+
".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
|
|
442
|
+
".mneme-xside{flex:none;width:236px;min-width:0;min-height:0;overflow-y:auto;padding:12px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:8px}",
|
|
443
|
+
".mneme-xside--filter{width:214px}",
|
|
444
|
+
".mneme-xbrowse{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
|
|
401
445
|
".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
402
446
|
".mneme-xsearch{width:100%}",
|
|
403
447
|
".mneme-xselect{flex:1;min-width:0}",
|
|
@@ -422,35 +466,17 @@ window.__ModuleLoader__.load({
|
|
|
422
466
|
".mneme-xdot{flex:none;display:inline-block;width:8px;height:8px;border-radius:50%;background:currentColor}",
|
|
423
467
|
".mneme-xdot--all{background:transparent;border:1.5px solid currentColor;opacity:.55;box-sizing:border-box}",
|
|
424
468
|
".mneme-xitem .mneme-xdot{align-self:center}",
|
|
425
|
-
// ---
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
".mneme-
|
|
429
|
-
".mneme-
|
|
430
|
-
".mneme-card--search{width:236px;flex:none}",
|
|
431
|
-
".mneme-card--tree{width:260px;flex:none}",
|
|
432
|
-
".mneme-card--detail{flex:1;min-width:0}",
|
|
469
|
+
// --- detail column (preview pane sits BELOW the timeline: the list is
|
|
470
|
+
// the primary scan target, the detail grows only when something is
|
|
471
|
+
// selected so the empty state never eats the viewport) ---
|
|
472
|
+
".mneme-xdetail{flex:none;max-height:56%;min-height:0;overflow-y:auto;padding:12px 20px 16px;border-top:1px solid var(--dsw-alias-border-l2)}",
|
|
473
|
+
".mneme-xtree{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
|
|
433
474
|
".mneme-xdinner{max-width:720px}",
|
|
434
475
|
".mneme-xdtitle{font-size:16px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);margin-bottom:10px;word-break:break-word}",
|
|
435
476
|
".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
436
477
|
".mneme-xdsrc{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}",
|
|
437
478
|
".mneme-xdcontent{margin-top:14px;font-size:14px;line-height:1.75;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}",
|
|
438
479
|
".mneme-xdactions{display:flex;gap:8px;margin-top:18px}",
|
|
439
|
-
// --- wiki-link backlinks panel (detail pane footer) ---
|
|
440
|
-
".mneme-backlinks{margin-top:18px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l1);display:flex;flex-direction:column;gap:12px}",
|
|
441
|
-
".mneme-bl-block{display:flex;flex-direction:column;gap:4px}",
|
|
442
|
-
".mneme-bl-head{font-size:12px;font-weight:500;color:var(--dsw-alias-label-tertiary)}",
|
|
443
|
-
".mneme-bl-list{display:flex;flex-wrap:wrap;gap:6px}",
|
|
444
|
-
".mneme-bl-link{display:inline-flex;align-items:center;gap:6px;max-width:100%;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;padding:2px 8px;border-radius:6px;text-align:left}",
|
|
445
|
-
".mneme-bl-link:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
446
|
-
".mneme-bl-link.mneme-bl-link--dim{color:var(--dsw-alias-label-tertiary);cursor:default}",
|
|
447
|
-
".mneme-bl-link.mneme-bl-link--dim:hover{background:none}",
|
|
448
|
-
".mneme-bl-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
449
|
-
".mneme-bl-meta{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary)}",
|
|
450
|
-
".mneme-bl-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
|
|
451
|
-
// --- inline [[wiki-link]] links inside the detail content ---
|
|
452
|
-
".mneme-wikilink{display:inline;padding:0 2px;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:inherit;line-height:inherit;text-decoration:underline;text-underline-offset:2px;border-radius:3px}",
|
|
453
|
-
".mneme-wikilink:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 8%,transparent)}",
|
|
454
480
|
// --- graph sub-view (fills the content area under the tabs) ---
|
|
455
481
|
".mneme-graph{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;padding:12px 16px 16px;box-sizing:border-box}",
|
|
456
482
|
".mneme-graphbar{display:flex;gap:8px;align-items:center;flex:none;margin-bottom:10px}",
|
|
@@ -472,38 +498,36 @@ window.__ModuleLoader__.load({
|
|
|
472
498
|
".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
|
|
473
499
|
".mneme-gs-link{display:block;width:100%;text-align:left;border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;padding:3px 6px;border-radius:6px;word-break:break-word}",
|
|
474
500
|
".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
475
|
-
// ---
|
|
476
|
-
".mneme-
|
|
477
|
-
".mneme-
|
|
478
|
-
".mneme-
|
|
479
|
-
".mneme-
|
|
480
|
-
".mneme-
|
|
481
|
-
".mneme-
|
|
482
|
-
".mneme-
|
|
483
|
-
".mneme-
|
|
484
|
-
".mneme-
|
|
485
|
-
".mneme-
|
|
486
|
-
".mneme-
|
|
487
|
-
".mneme-
|
|
488
|
-
".mneme-
|
|
489
|
-
".mneme-
|
|
490
|
-
".mneme-
|
|
491
|
-
".mneme-
|
|
492
|
-
".mneme-
|
|
493
|
-
".mneme-
|
|
494
|
-
".mneme-
|
|
495
|
-
".mneme-
|
|
496
|
-
".mneme-
|
|
497
|
-
".mneme-
|
|
498
|
-
".mneme-
|
|
499
|
-
".mneme-
|
|
500
|
-
|
|
501
|
-
".mneme-
|
|
502
|
-
".mneme-
|
|
503
|
-
// --- settings sub-view ---
|
|
504
|
-
".mneme-xsettings{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
|
|
501
|
+
// --- entity explorer: rail + layered detail (entities → attrs → relations → graph) ---
|
|
502
|
+
".mneme-ent{flex:1;min-height:0;display:flex;overflow:hidden}",
|
|
503
|
+
".mneme-entrail{flex:none;width:224px;min-height:0;overflow-y:auto;padding:12px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:4px}",
|
|
504
|
+
".mneme-enttype{display:flex;align-items:center;gap:6px;width:100%;padding:8px 8px 2px;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;line-height:16px;text-align:left;letter-spacing:.02em}",
|
|
505
|
+
".mneme-entitem{display:flex;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
|
|
506
|
+
".mneme-entitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
507
|
+
".mneme-entitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
|
|
508
|
+
".mneme-entname{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
509
|
+
".mneme-entmentions{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
510
|
+
".mneme-entdetail{flex:1;min-width:0;min-height:0;overflow-y:auto;padding:20px 24px 32px;box-sizing:border-box}",
|
|
511
|
+
".mneme-entinner{max-width:720px}",
|
|
512
|
+
".mneme-enttitle{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);word-break:break-word}",
|
|
513
|
+
".mneme-enttitle .mneme-xdot{width:10px;height:10px}",
|
|
514
|
+
".mneme-entmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
515
|
+
".mneme-layer{margin-top:24px}",
|
|
516
|
+
".mneme-layerhead{display:flex;align-items:baseline;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:10px}",
|
|
517
|
+
".mneme-layercount{font-size:12px;font-weight:400;color:var(--dsw-alias-label-tertiary)}",
|
|
518
|
+
".mneme-attrgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}",
|
|
519
|
+
".mneme-attrcard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 12px}",
|
|
520
|
+
".mneme-attrkey{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
521
|
+
".mneme-attrval{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word}",
|
|
522
|
+
".mneme-relrow{display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;border:none;border-radius:10px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:20px;text-align:left}",
|
|
523
|
+
".mneme-relrow:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
524
|
+
".mneme-reltype{flex:none;font-size:11px;line-height:16px;color:var(--dsw-alias-state-business-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);border-radius:6px;padding:0 6px}",
|
|
525
|
+
".mneme-relname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
526
|
+
// --- settings: quiet stacked sections, one concern per section ---
|
|
527
|
+
".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
|
|
528
|
+
".mneme-set-inner{max-width:680px}",
|
|
505
529
|
".mneme-set-sec{padding:20px 0 24px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
506
|
-
".mneme-
|
|
530
|
+
".mneme-set-sec:last-child{border-bottom:none}",
|
|
507
531
|
".mneme-set-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px}",
|
|
508
532
|
".mneme-set-desc{font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);margin-bottom:14px}",
|
|
509
533
|
".mneme-set-input{box-sizing:border-box;width:100%;height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none;margin-bottom:10px;transition:border-color .12s,box-shadow .12s}",
|
|
@@ -514,6 +538,7 @@ window.__ModuleLoader__.load({
|
|
|
514
538
|
".mneme-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
515
539
|
".mneme-btn:disabled{opacity:.5;cursor:default}",
|
|
516
540
|
".mneme-saved{font-size:12px;color:var(--dsw-alias-state-success,#2a7)}",
|
|
541
|
+
// rule rows: numbered chips + hover-revealed delete
|
|
517
542
|
".mneme-set-row{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:8px;background:var(--dsw-alias-bg-base,transparent)}",
|
|
518
543
|
".mneme-set-idx{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}",
|
|
519
544
|
".mneme-set-ruletext{flex:1;min-width:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);word-break:break-word}",
|
|
@@ -523,47 +548,46 @@ window.__ModuleLoader__.load({
|
|
|
523
548
|
".mneme-set-cmd{flex:1;min-width:0}",
|
|
524
549
|
".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--dsw-alias-state-business-primary)}",
|
|
525
550
|
".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);margin-top:1px;word-break:break-word}",
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
".mneme-ov-wrap{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:12px;padding:12px 16px 28px}",
|
|
529
|
-
".mneme-ov-profile{gap:8px}",
|
|
530
|
-
".mneme-ov-prow{display:flex;flex-direction:column;gap:2px;padding:4px 0;border-top:1px solid var(--dsw-alias-border-l1)}",
|
|
531
|
-
".mneme-ov-prow:first-of-type{border-top:none}",
|
|
532
|
-
".mneme-ov-ptitle{font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
533
|
-
".mneme-ov-ppreview{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
534
|
-
".mneme-ov-dist{display:flex;flex-direction:column;gap:6px}",
|
|
535
|
-
".mneme-ov-distrow{display:flex;align-items:center;gap:8px}",
|
|
536
|
-
".mneme-ov-distlabel{flex:none;width:76px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
537
|
-
".mneme-ov-distcount{flex:none;width:34px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-align:right}",
|
|
538
|
-
".mneme-ov-disttrack{flex:1;min-width:0;height:8px;border-radius:4px;background:var(--dsw-alias-border-l2)}",
|
|
539
|
-
".mneme-ov-distbar{height:100%;min-width:2px;border-radius:4px;background:var(--dsw-alias-state-business-primary)}",
|
|
540
|
-
".mneme-ov-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px}",
|
|
541
|
-
".mneme-ov-tile{box-sizing:border-box;text-align:left;border:none;cursor:pointer;font-family:inherit;gap:6px;overflow:hidden}",
|
|
542
|
-
".mneme-ov-tile:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
543
|
-
".mneme-ov-tilehead{display:flex;align-items:center;gap:6px}",
|
|
544
|
-
".mneme-ov-tiletitle{min-width:0;font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
545
|
-
".mneme-ov-tilecount{flex:none;margin-left:auto;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
546
|
-
".mneme-ov-badge{flex:none;font-size:11px;line-height:16px;padding:0 6px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary)}",
|
|
547
|
-
".mneme-ov-badge--dim{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}",
|
|
548
|
-
".mneme-ov-tilelatest{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
549
|
-
".mneme-ov-trend{display:flex;align-items:flex-end;gap:8px;padding:8px 4px 0}",
|
|
550
|
-
".mneme-ov-trendcol{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:4px}",
|
|
551
|
-
".mneme-ov-trendbar{width:min(100%,30px);border-radius:4px 4px 0 0;background:var(--dsw-alias-state-business-primary)}",
|
|
552
|
-
".mneme-ov-trenddate{font-size:10px;line-height:12px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:nowrap}",
|
|
551
|
+
// --- settings sub-view ---
|
|
552
|
+
".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
|
|
553
553
|
// --- hero fallback: full-viewport memory library when no tab ring exists ---
|
|
554
554
|
// The host hides the whole conversation tab ring while a session is
|
|
555
555
|
// blank (hero screen), so the sidebar entry cannot activate the tab
|
|
556
556
|
// there. This surface reuses the exact MemoryExplorer UI at full size —
|
|
557
557
|
// not a side drawer — so the library stays reachable from any state.
|
|
558
558
|
".mneme-overlay{position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);animation:mneme-fadein .12s ease-out}",
|
|
559
|
-
|
|
560
|
-
// 落在同一区域——宿主控制按钮浮在 web 内容最上层,抢占了 overlay"关闭"按钮的
|
|
561
|
-
// 点击热区,导致记忆窗口无法关闭。改为左对齐(标题 + 关闭按钮并排),关闭按钮
|
|
562
|
-
// 离开右上角宿主控制按钮区,任何窗口尺寸/宿主下都可见可点。
|
|
563
|
-
".mneme-overlaybar{flex:none;display:flex;align-items:center;justify-content:flex-start;gap:12px;height:44px;padding:0 12px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
559
|
+
".mneme-overlaybar{flex:none;display:flex;align-items:center;justify-content:space-between;height:44px;padding:0 12px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
564
560
|
".mneme-overlaytitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
565
561
|
".mneme-overlaybody{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
|
|
566
|
-
"@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}"
|
|
562
|
+
"@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}",
|
|
563
|
+
// --- explorer refresh: paged month tree, sticky headers, inline icons ---
|
|
564
|
+
".mneme-vtabico{flex:none;opacity:.85}",
|
|
565
|
+
".mneme-xsearchwrap{position:relative;flex:none}",
|
|
566
|
+
".mneme-xsearchico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--dsw-alias-label-tertiary);pointer-events:none}",
|
|
567
|
+
".mneme-xsearchwrap .mneme-xsearch{padding-left:27px}",
|
|
568
|
+
".mneme-footbtn{display:inline-flex;align-items:center;gap:4px}",
|
|
569
|
+
".mneme-xmonth{position:sticky;top:0;z-index:2;background:var(--dsw-alias-bg-layer-1);display:flex;align-items:center;gap:6px}",
|
|
570
|
+
".mneme-xmonthcount{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-weight:400;font-variant-numeric:tabular-nums}",
|
|
571
|
+
".mneme-xcaret{display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary)}",
|
|
572
|
+
".mneme-xmore{display:flex;justify-content:center;align-items:center;padding:10px 0 24px;min-height:20px}",
|
|
573
|
+
".mneme-xemptyico{display:block;margin:0 auto 6px;opacity:.7}",
|
|
574
|
+
// --- status sub-view: responsive stat-card grid (auto-fill, ~220px min) ---
|
|
575
|
+
".mneme-status{flex:1;min-height:0;overflow-y:auto;padding:16px 20px 32px;box-sizing:border-box}",
|
|
576
|
+
".mneme-statusgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;max-width:960px}",
|
|
577
|
+
".mneme-statuscard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px}",
|
|
578
|
+
".mneme-statusnum{font-size:24px;font-weight:600;line-height:32px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}",
|
|
579
|
+
".mneme-statuscap{margin-top:4px;font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);word-break:break-word}",
|
|
580
|
+
// --- destructive actions: red outline = delete, solid red = confirm ---
|
|
581
|
+
".mneme-btndanger{color:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33)}",
|
|
582
|
+
".mneme-btndanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 12%,transparent)}",
|
|
583
|
+
".mneme-btndangerconfirm{background:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33);color:#fff}",
|
|
584
|
+
".mneme-btndangerconfirm:hover{filter:brightness(.9)}",
|
|
585
|
+
// --- settings cards: boxed cards for the runtime-mode and external-API
|
|
586
|
+
// sections — each card owns its fetch/PUT state, so it renders as a
|
|
587
|
+
// self-contained unit inside the stacked settings view ---
|
|
588
|
+
".mneme-set-card{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px;margin-bottom:12px}",
|
|
589
|
+
".mneme-set-token{font-family:monospace;font-size:12px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);word-break:break-all;user-select:all}",
|
|
590
|
+
".mneme-set-hint{font-size:12px;color:var(--dsw-alias-label-tertiary)}"
|
|
567
591
|
].join("\n");
|
|
568
592
|
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) {
|
|
569
593
|
const tag = document.createElement("style");
|
|
@@ -599,32 +623,28 @@ window.__ModuleLoader__.load({
|
|
|
599
623
|
project: "#22c55e",
|
|
600
624
|
decision: "#3b82f6",
|
|
601
625
|
summary: "#a855f7",
|
|
602
|
-
user: "#f59e0b",
|
|
603
|
-
fact: "#22c55e",
|
|
604
626
|
history: "#94a3b8"
|
|
605
627
|
};
|
|
606
628
|
function memoryTypeColor(type) {
|
|
607
629
|
return MEMORY_TYPE_COLORS[type] || "#94a3b8";
|
|
608
630
|
}
|
|
609
631
|
|
|
610
|
-
// Compact date for the detail meta ("2026/
|
|
632
|
+
// Compact date for the detail meta ("2026/8/16"); the full timestamp
|
|
611
633
|
// stays in the tooltip.
|
|
612
634
|
function formatDateShort(value) {
|
|
613
635
|
if (!value) return "—";
|
|
614
636
|
const date = new Date(value);
|
|
615
637
|
return Number.isNaN(date.getTime()) ? "—" : date.toLocaleDateString();
|
|
616
638
|
}
|
|
639
|
+
// Graph canvas space: the svg's fixed viewBox. The simulation runs in
|
|
640
|
+
// these user units — never in CSS pixels — so node positions always land
|
|
641
|
+
// inside the drawn area; the browser scales the viewBox to whatever the
|
|
642
|
+
// element's CSS size is (small window, hidden tab, maximized #72).
|
|
643
|
+
const VIEW_W = 380, VIEW_H = 300;
|
|
644
|
+
|
|
617
645
|
function nodeRadius(n) {
|
|
618
646
|
// mention_count → area-ish growth, clamped so hubs stay legible.
|
|
619
|
-
|
|
620
|
-
// v0.7.0 heat: hot entities render slightly larger; cold → base.
|
|
621
|
-
if (n.heat == null) return base;
|
|
622
|
-
return base + (Math.max(0, Math.min(1, n.heat)) - 0.5) * 4;
|
|
623
|
-
}
|
|
624
|
-
function nodeOpacity(n) {
|
|
625
|
-
// v0.7.0 heat: hot entities are bright (opacity 1), cold fade to 0.4.
|
|
626
|
-
if (n.heat == null) return 1;
|
|
627
|
-
return 0.4 + 0.6 * Math.max(0, Math.min(1, n.heat));
|
|
647
|
+
return 7 + Math.min(20, Math.max(1, n.mention_count ?? 1)) * 0.55;
|
|
628
648
|
}
|
|
629
649
|
|
|
630
650
|
// Deterministic golden-angle spiral: no two nodes start overlapping, and
|
|
@@ -639,100 +659,17 @@ window.__ModuleLoader__.load({
|
|
|
639
659
|
});
|
|
640
660
|
}
|
|
641
661
|
|
|
642
|
-
// ---
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
let m;
|
|
653
|
-
while ((m = re.exec(text)) !== null) {
|
|
654
|
-
const parts = m[1].split("|");
|
|
655
|
-
if (parts.length > 2) continue; // 多管道 → 非法,保留字面文本
|
|
656
|
-
const target = (parts.length === 2 ? parts[1] : parts[0]).trim();
|
|
657
|
-
if (!target) continue; // 空目标 → 非法
|
|
658
|
-
if (m.index > last) out.push(text.slice(last, m.index));
|
|
659
|
-
out.push(h("button", {
|
|
660
|
-
key: `w${key++}`,
|
|
661
|
-
type: "button",
|
|
662
|
-
className: "mneme-wikilink",
|
|
663
|
-
title: target,
|
|
664
|
-
onClick: () => onClick(target)
|
|
665
|
-
}, parts[0].trim() || target));
|
|
666
|
-
last = m.index + m[0].length;
|
|
667
|
-
}
|
|
668
|
-
if (last < text.length) out.push(text.slice(last));
|
|
669
|
-
return out.length ? out : text;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Backlinks panel: the two wiki-link relation blocks under the detail pane.
|
|
673
|
-
// Backlinks = memories whose content links to this one; forward = memories
|
|
674
|
-
// this one links to (unresolved targets surface as dim, non-clickable).
|
|
675
|
-
// Mounted per-selection (the surrounding Fragment keys on selected.id), so
|
|
676
|
-
// state resets and the fetches re-run on every switch.
|
|
677
|
-
function BacklinksPanel({ memory, t, onJump }) {
|
|
678
|
-
const [back, setBack] = useState(null); // null = loading
|
|
679
|
-
const [forward, setForward] = useState(null);
|
|
680
|
-
|
|
681
|
-
useEffect(() => {
|
|
682
|
-
let cancelled = false;
|
|
683
|
-
if (!memory || !memory.id) return;
|
|
684
|
-
apiFetch(`/api/dsh-mneme/wikilinks/backlinks?id=${encodeURIComponent(memory.id)}`)
|
|
685
|
-
.then((r) => (r.ok ? r.json() : { backlinks: [] }))
|
|
686
|
-
.then((j) => { if (!cancelled) setBack(Array.isArray(j.backlinks) ? j.backlinks : []); })
|
|
687
|
-
.catch(() => { if (!cancelled) setBack([]); });
|
|
688
|
-
apiFetch(`/api/dsh-mneme/wikilinks/forward?id=${encodeURIComponent(memory.id)}`)
|
|
689
|
-
.then((r) => (r.ok ? r.json() : { links: [] }))
|
|
690
|
-
.then((j) => { if (!cancelled) setForward(Array.isArray(j.links) ? j.links : []); })
|
|
691
|
-
.catch(() => { if (!cancelled) setForward([]); });
|
|
692
|
-
return () => { cancelled = true; };
|
|
693
|
-
}, [memory && memory.id]);
|
|
694
|
-
|
|
695
|
-
if (!memory || !memory.id) return null;
|
|
696
|
-
|
|
697
|
-
const loading = back === null || forward === null;
|
|
698
|
-
const row = (link, i) => {
|
|
699
|
-
const canJump = !!link.id;
|
|
700
|
-
return h("button", {
|
|
701
|
-
key: link.id ?? `unresolved-${i}`,
|
|
702
|
-
type: "button",
|
|
703
|
-
className: canJump ? "mneme-bl-link" : "mneme-bl-link mneme-bl-link--dim",
|
|
704
|
-
title: canJump ? t("memory.card.open") : t("memory.wikilink.unresolved"),
|
|
705
|
-
disabled: !canJump,
|
|
706
|
-
onClick: () => onJump && onJump({ id: link.id })
|
|
707
|
-
},
|
|
708
|
-
h("span", { className: "mneme-bl-title" }, link.title || "—"),
|
|
709
|
-
link.type && h("span", { className: "mneme-bl-meta" }, typeLabel(t, link.type))
|
|
710
|
-
);
|
|
711
|
-
};
|
|
712
|
-
const section = (label, items) =>
|
|
713
|
-
h("div", { className: "mneme-bl-block" },
|
|
714
|
-
h("div", { className: "mneme-bl-head" }, label),
|
|
715
|
-
loading
|
|
716
|
-
? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.loading"))
|
|
717
|
-
: items.length === 0
|
|
718
|
-
? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.empty"))
|
|
719
|
-
: h("div", { className: "mneme-bl-list" }, items.map(row))
|
|
720
|
-
);
|
|
721
|
-
|
|
722
|
-
return h("div", { className: "mneme-backlinks" },
|
|
723
|
-
section(t("memory.wikilink.backlinks"), back || []),
|
|
724
|
-
section(t("memory.wikilink.forward"), forward || [])
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// --- Graph view: ego-graph of one entity, zero-dependency SVG force layout ---
|
|
729
|
-
// The DSH client module table only whitelists platform modules, so a graph
|
|
730
|
-
// library like vis-network cannot be required from a plugin. A hand-rolled
|
|
731
|
-
// spring simulation (repulsion + edge springs + center gravity, damped) is
|
|
732
|
-
// plenty for the ≤40 nodes the ego API returns.
|
|
733
|
-
function GraphPanel({ t, focusEntity, onJumpMemory }) {
|
|
662
|
+
// --- Entity explorer: the entity-gene layers made visible ---
|
|
663
|
+
// Rail = the entity directory (grouped by type, mention-ranked inside a
|
|
664
|
+
// type). Detail = the layered structure from docs/ENTITIES.md:
|
|
665
|
+
// ① 属性 current snapshot of entity_attrs (saveAttr invalidates old rows)
|
|
666
|
+
// ② 关系 entity_relations touching the entity (from the 1-hop ego walk)
|
|
667
|
+
// ③ 关联记忆 memories recalled through the entity: prefix search
|
|
668
|
+
// ④ 关系图谱 the same ego graph the old graph-only tab drew
|
|
669
|
+
function EntityPanel({ t, focusEntity, onJumpMemory }) {
|
|
670
|
+
const [entities, setEntities] = useState(null); // null = still loading
|
|
671
|
+
const [railQuery, setRailQuery] = useState("");
|
|
734
672
|
const [entityName, setEntityName] = useState(focusEntity || "");
|
|
735
|
-
const [inputValue, setInputValue] = useState(focusEntity || "");
|
|
736
673
|
const [depth, setDepth] = useState(1);
|
|
737
674
|
const [data, setData] = useState(null);
|
|
738
675
|
const [status, setStatus] = useState("idle"); // idle | loading | ready | notfound | error
|
|
@@ -745,14 +682,29 @@ window.__ModuleLoader__.load({
|
|
|
745
682
|
const viewRef = useRef({ x: 0, y: 0, k: 1 }); // pan/zoom viewport, viewBox units
|
|
746
683
|
const [frame, setFrame] = useState(0); // re-render tick driven by the simulation
|
|
747
684
|
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
let cancelled = false;
|
|
687
|
+
apiFetch("/api/dsh-mneme/entities?limit=500")
|
|
688
|
+
.then((r) => (r.ok ? r.json() : { entities: [] }))
|
|
689
|
+
.then((j) => { if (!cancelled) setEntities(Array.isArray(j.entities) ? j.entities : []); })
|
|
690
|
+
.catch(() => { if (!cancelled) setEntities([]); });
|
|
691
|
+
return () => { cancelled = true; };
|
|
692
|
+
}, []);
|
|
693
|
+
|
|
748
694
|
useEffect(() => {
|
|
749
695
|
if (focusEntity && focusEntity !== entityName) {
|
|
750
696
|
setEntityName(focusEntity);
|
|
751
|
-
setInputValue(focusEntity);
|
|
752
697
|
}
|
|
753
698
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
754
699
|
}, [focusEntity]);
|
|
755
700
|
|
|
701
|
+
// Auto-select the most recently seen entity so the layers are never a
|
|
702
|
+
// dead end on first open (the rail still allows picking another one).
|
|
703
|
+
useEffect(() => {
|
|
704
|
+
if (!entities || entityName || entities.length === 0) return;
|
|
705
|
+
setEntityName(entities[0].name);
|
|
706
|
+
}, [entities, entityName]);
|
|
707
|
+
|
|
756
708
|
const load = useCallback(async (name, d) => {
|
|
757
709
|
if (!name) { setData(null); setStatus("idle"); return; }
|
|
758
710
|
setStatus("loading");
|
|
@@ -772,31 +724,32 @@ window.__ModuleLoader__.load({
|
|
|
772
724
|
|
|
773
725
|
useEffect(() => { load(entityName, depth); }, [load, entityName, depth]);
|
|
774
726
|
|
|
775
|
-
//
|
|
727
|
+
// Layer data for the selected entity: current attrs + related memories.
|
|
776
728
|
useEffect(() => {
|
|
777
729
|
setAttrs([]);
|
|
778
730
|
setRelated([]);
|
|
779
|
-
if (!
|
|
780
|
-
const name = selected.node.name;
|
|
731
|
+
if (!entityName) return;
|
|
781
732
|
let cancelled = false;
|
|
782
|
-
apiFetch(`/api/dsh-mneme/semantic/graph/entity-attrs?entity=${encodeURIComponent(
|
|
733
|
+
apiFetch(`/api/dsh-mneme/semantic/graph/entity-attrs?entity=${encodeURIComponent(entityName)}`)
|
|
783
734
|
.then((r) => (r.ok ? r.json() : { attrs: [] }))
|
|
784
735
|
.then((j) => { if (!cancelled) setAttrs(Array.isArray(j.attrs) ? j.attrs : []); })
|
|
785
736
|
.catch(() => {});
|
|
786
|
-
apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent("entity:" +
|
|
737
|
+
apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent("entity:" + entityName)}&limit=10`)
|
|
787
738
|
.then((r) => (r.ok ? r.json() : { items: [] }))
|
|
788
739
|
.then((j) => { if (!cancelled) setRelated(Array.isArray(j.items) ? j.items : []); })
|
|
789
740
|
.catch(() => {});
|
|
790
741
|
return () => { cancelled = true; };
|
|
791
|
-
}, [
|
|
742
|
+
}, [entityName]);
|
|
792
743
|
|
|
793
744
|
// Simulation: run in rAF against posRef, tick React only every few frames.
|
|
794
745
|
// Re-seeds when data changes; dragging writes straight into posRef.
|
|
795
746
|
useEffect(() => {
|
|
796
747
|
if (!data || data.nodes.length === 0) return;
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
748
|
+
// Bounds in viewBox units: measuring clientWidth here put the gravity
|
|
749
|
+
// center and clamp range outside the 380-wide viewBox once the window
|
|
750
|
+
// (or its hidden-then-shown container) got wide, and every node was
|
|
751
|
+
// clipped off-canvas — the maximized-invisible bug (#72).
|
|
752
|
+
posRef.current = initialPositions(data.nodes, VIEW_W, VIEW_H);
|
|
800
753
|
const byId = new Map(posRef.current.map((n) => [n.id, n]));
|
|
801
754
|
const edges = data.edges;
|
|
802
755
|
let raf = 0;
|
|
@@ -829,12 +782,12 @@ window.__ModuleLoader__.load({
|
|
|
829
782
|
let energy = 0;
|
|
830
783
|
for (const node of nodes) {
|
|
831
784
|
// gentle gravity toward center keeps the cloud from drifting off-canvas
|
|
832
|
-
node.vx += (
|
|
833
|
-
node.vy += (
|
|
785
|
+
node.vx += (VIEW_W / 2 - node.x) * 0.002;
|
|
786
|
+
node.vy += (VIEW_H / 2 - node.y) * 0.002;
|
|
834
787
|
if (node.pinned || node === dragRef.current?.node) { node.vx = 0; node.vy = 0; continue; }
|
|
835
788
|
node.vx *= 0.85; node.vy *= 0.85;
|
|
836
|
-
node.x = Math.max(nodeRadius(node) + 4, Math.min(
|
|
837
|
-
node.y = Math.max(nodeRadius(node) + 14, Math.min(
|
|
789
|
+
node.x = Math.max(nodeRadius(node) + 4, Math.min(VIEW_W - nodeRadius(node) - 4, node.x + node.vx));
|
|
790
|
+
node.y = Math.max(nodeRadius(node) + 14, Math.min(VIEW_H - nodeRadius(node) - 14, node.y + node.vy));
|
|
838
791
|
energy += Math.abs(node.vx) + Math.abs(node.vy);
|
|
839
792
|
}
|
|
840
793
|
n++;
|
|
@@ -854,7 +807,7 @@ window.__ModuleLoader__.load({
|
|
|
854
807
|
const origX = node.x, origY = node.y;
|
|
855
808
|
const svg = svgRef.current;
|
|
856
809
|
const rect = svg.getBoundingClientRect();
|
|
857
|
-
const scale =
|
|
810
|
+
const scale = VIEW_W / Math.max(1, rect.width) / viewRef.current.k; // viewBox units per css px, zoom-aware
|
|
858
811
|
const onMove = (ev) => {
|
|
859
812
|
dragRef.current.moved = true;
|
|
860
813
|
node.x = origX + (ev.clientX - startX) * scale;
|
|
@@ -870,6 +823,12 @@ window.__ModuleLoader__.load({
|
|
|
870
823
|
window.addEventListener("mouseup", onUp);
|
|
871
824
|
};
|
|
872
825
|
|
|
826
|
+
const onNodeClick = (node) => {
|
|
827
|
+
if (dragRef.current?.moved) return; // it was a drag, not a click
|
|
828
|
+
setSelected({ kind: "node", node });
|
|
829
|
+
};
|
|
830
|
+
const onEdgeClick = (edge) => setSelected({ kind: "edge", edge });
|
|
831
|
+
|
|
873
832
|
// --- canvas pan & zoom ---
|
|
874
833
|
// The viewport (translate + scale) lives in viewRef so the simulation
|
|
875
834
|
// and the handlers share it without re-mounting; every mutation bumps
|
|
@@ -934,39 +893,71 @@ window.__ModuleLoader__.load({
|
|
|
934
893
|
setFrame((f) => f + 1);
|
|
935
894
|
};
|
|
936
895
|
|
|
937
|
-
const onNodeClick = (node) => {
|
|
938
|
-
if (dragRef.current?.moved) return; // it was a drag, not a click
|
|
939
|
-
setSelected({ kind: "node", node });
|
|
940
|
-
};
|
|
941
|
-
const onEdgeClick = (edge) => setSelected({ kind: "edge", edge });
|
|
942
|
-
|
|
943
896
|
const nodes = posRef.current;
|
|
944
897
|
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
945
|
-
const VIEW_W = 380, VIEW_H = 300;
|
|
946
898
|
|
|
899
|
+
// --- rail: entity directory grouped by type ---
|
|
900
|
+
const q = railQuery.trim().toLowerCase();
|
|
901
|
+
const visibleEntities = (entities || []).filter((e) => !q || (e.name || "").toLowerCase().includes(q));
|
|
902
|
+
const ENTITY_TYPE_ORDER = ["person", "project", "concept", "technology", "organization"];
|
|
903
|
+
const groups = [];
|
|
904
|
+
const byType = new Map();
|
|
905
|
+
for (const e of visibleEntities) {
|
|
906
|
+
const key = ENTITY_TYPE_ORDER.includes(e.type) ? e.type : "other";
|
|
907
|
+
if (!byType.has(key)) { const g = { key, items: [] }; byType.set(key, g); groups.push(g); }
|
|
908
|
+
byType.get(key).items.push(e);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const entity = entities?.find((e) => e.name === entityName) || null;
|
|
912
|
+
const rootEdges = (data?.edges || []).filter((e) => e.from === data?.root?.id || e.to === data?.root?.id);
|
|
913
|
+
const graphStatus = status === "ready" && data && data.nodes.length <= 1 ? "empty" : status;
|
|
914
|
+
|
|
915
|
+
const rail = h("div", { className: "mneme-entrail" },
|
|
916
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.view.label")),
|
|
917
|
+
h("input", {
|
|
918
|
+
className: "mneme-search mneme-xsearch",
|
|
919
|
+
placeholder: t("memory.graph.placeholder"),
|
|
920
|
+
value: railQuery,
|
|
921
|
+
onChange: (e) => setRailQuery(e.target.value)
|
|
922
|
+
}),
|
|
923
|
+
entities === null && h("div", { className: "mneme-xempty" }, "…"),
|
|
924
|
+
entities !== null && entities.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.entities.none")),
|
|
925
|
+
entities !== null && entities.length > 0 && h("div", { className: "mneme-xcount" },
|
|
926
|
+
t("memory.entities.count").replace("{n}", String(entities.length))),
|
|
927
|
+
groups.map((g) => h("div", { key: g.key },
|
|
928
|
+
h("div", { className: "mneme-enttype" },
|
|
929
|
+
h("span", { className: "mneme-xdot", style: { color: typeColor(g.key === "other" ? null : g.key) }, "aria-hidden": "true" }),
|
|
930
|
+
entityTypeLabel(t, g.key)
|
|
931
|
+
),
|
|
932
|
+
g.items.map((e) => h("button", {
|
|
933
|
+
key: e.id,
|
|
934
|
+
className: e.name === entityName ? "mneme-entitem mneme-active" : "mneme-entitem",
|
|
935
|
+
onClick: () => setEntityName(e.name)
|
|
936
|
+
},
|
|
937
|
+
h("span", { className: "mneme-entname" }, e.name),
|
|
938
|
+
h("span", { className: "mneme-entmentions" }, `×${e.mention_count ?? 1}`)
|
|
939
|
+
))
|
|
940
|
+
))
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
// side card for a clicked (non-root) node or edge inside the graph layer
|
|
947
944
|
const side = selected?.kind === "node"
|
|
948
945
|
? h("div", { className: "mneme-graphside" },
|
|
949
946
|
h("div", { className: "mneme-gs-title" },
|
|
950
947
|
h("span", null, selected.node.name),
|
|
951
948
|
h("span", { className: "mneme-gs-meta" },
|
|
952
|
-
`${
|
|
949
|
+
`${entityTypeLabel(t, selected.node.type || "concept")} · ★${selected.node.mention_count ?? 1}`)
|
|
953
950
|
),
|
|
951
|
+
selected.node.name !== entityName && h("button", {
|
|
952
|
+
className: "mneme-footbtn",
|
|
953
|
+
onClick: () => setEntityName(selected.node.name)
|
|
954
|
+
}, t("memory.graph.viewInGraph")),
|
|
954
955
|
attrs.length > 0 && h("div", null,
|
|
955
956
|
h("div", { className: "mneme-gs-meta" }, t("memory.graph.attrs")),
|
|
956
957
|
attrs.map((a, i) => h("div", { key: i, className: "mneme-gs-attr" },
|
|
957
958
|
h("span", { className: "mneme-gs-attrkey" }, `${a.key}:`),
|
|
958
959
|
h("span", { className: "mneme-gs-attrval" }, a.value)
|
|
959
960
|
))
|
|
960
|
-
),
|
|
961
|
-
related.length > 0 && h("div", { style: { marginTop: 8 } },
|
|
962
|
-
h("div", { className: "mneme-gs-meta" }, t("memory.graph.related")),
|
|
963
|
-
related.map((m) => h("button", {
|
|
964
|
-
key: m.id,
|
|
965
|
-
type: "button",
|
|
966
|
-
className: "mneme-gs-link",
|
|
967
|
-
title: t("memory.card.open"),
|
|
968
|
-
onClick: () => onJumpMemory && onJumpMemory(m)
|
|
969
|
-
}, m.title || m.content?.slice(0, 60)))
|
|
970
961
|
)
|
|
971
962
|
)
|
|
972
963
|
: selected?.kind === "edge"
|
|
@@ -984,77 +975,140 @@ window.__ModuleLoader__.load({
|
|
|
984
975
|
)
|
|
985
976
|
: null;
|
|
986
977
|
|
|
987
|
-
|
|
988
|
-
h("div", { className: "mneme-
|
|
989
|
-
h("
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
status === "loading" && h("div", { className: "mneme-hint" }, t("memory.graph.loading")),
|
|
1010
|
-
status === "notfound" && h("div", { className: "mneme-hint" }, t("memory.graph.notFound")),
|
|
1011
|
-
status === "error" && h("div", { className: "mneme-hint" }, t("memory.panel.empty")),
|
|
1012
|
-
status === "ready" && (data.nodes.length <= 1
|
|
1013
|
-
? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
|
|
1014
|
-
: h(react.Fragment, null,
|
|
1015
|
-
h("svg", {
|
|
1016
|
-
ref: svgRef,
|
|
1017
|
-
className: "mneme-graphsvg",
|
|
1018
|
-
viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
|
|
1019
|
-
style: { height: 320 },
|
|
1020
|
-
onMouseDown: onSurfaceMouseDown
|
|
1021
|
-
},
|
|
1022
|
-
h("g", {
|
|
1023
|
-
transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
|
|
1024
|
-
},
|
|
1025
|
-
data.edges.map((e) => {
|
|
1026
|
-
const a = nodeById.get(e.from), b = nodeById.get(e.to);
|
|
1027
|
-
if (!a || !b) return null;
|
|
1028
|
-
return h("line", {
|
|
1029
|
-
key: e.id,
|
|
1030
|
-
x1: a.x, y1: a.y, x2: b.x, y2: b.y,
|
|
1031
|
-
className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
|
|
1032
|
-
onClick: () => onEdgeClick(e)
|
|
1033
|
-
});
|
|
1034
|
-
}),
|
|
1035
|
-
nodes.map((n) => h("g", {
|
|
1036
|
-
key: n.id,
|
|
1037
|
-
className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
|
|
1038
|
-
transform: `translate(${n.x},${n.y})`,
|
|
1039
|
-
onMouseDown: (e) => onNodeMouseDown(e, n),
|
|
1040
|
-
onClick: () => onNodeClick(n),
|
|
1041
|
-
"data-node": n.name
|
|
1042
|
-
},
|
|
1043
|
-
h("circle", { r: nodeRadius(n), fill: typeColor(n.type), fillOpacity: nodeOpacity(n) }),
|
|
1044
|
-
h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
|
|
1045
|
-
))
|
|
1046
|
-
)
|
|
978
|
+
const detail = h("div", { className: "mneme-entdetail" },
|
|
979
|
+
h("div", { className: "mneme-entinner" },
|
|
980
|
+
status === "idle" && h("div", { className: "mneme-hint" }, t("memory.entities.pick")),
|
|
981
|
+
status === "loading" && h("div", { className: "mneme-hint" }, t("memory.graph.loading")),
|
|
982
|
+
status === "notfound" && h("div", { className: "mneme-hint" }, t("memory.graph.notFound")),
|
|
983
|
+
status === "error" && h("div", { className: "mneme-hint" }, t("memory.panel.empty")),
|
|
984
|
+
status === "ready" && entity && h(react.Fragment, null,
|
|
985
|
+
h("div", { className: "mneme-enttitle" },
|
|
986
|
+
h("span", { className: "mneme-xdot", style: { color: typeColor(entity.type) }, "aria-hidden": "true" }),
|
|
987
|
+
entity.name
|
|
988
|
+
),
|
|
989
|
+
h("div", { className: "mneme-entmeta" },
|
|
990
|
+
h("span", null, entityTypeLabel(t, entity.type || "other")),
|
|
991
|
+
h("span", null, t("memory.entities.mentions").replace("{n}", String(entity.mention_count ?? 1))),
|
|
992
|
+
h("span", { title: formatDate(entity.last_seen) },
|
|
993
|
+
`${t("memory.entities.lastSeen")}: ${formatRelativeTime(entity.last_seen, t)}`)
|
|
994
|
+
),
|
|
995
|
+
// ① 属性 — current snapshot (valid_until IS NULL)
|
|
996
|
+
h("div", { className: "mneme-layer" },
|
|
997
|
+
h("div", { className: "mneme-layerhead" },
|
|
998
|
+
t("memory.entities.attrs"),
|
|
999
|
+
h("span", { className: "mneme-layercount" }, String(attrs.length))
|
|
1047
1000
|
),
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1001
|
+
attrs.length === 0
|
|
1002
|
+
? h("div", { className: "mneme-xempty" }, t("memory.settings.empty"))
|
|
1003
|
+
: h("div", { className: "mneme-attrgrid" },
|
|
1004
|
+
attrs.map((a, i) => h("div", { key: i, className: "mneme-attrcard", title: a.value },
|
|
1005
|
+
h("div", { className: "mneme-attrkey" }, a.key),
|
|
1006
|
+
h("div", { className: "mneme-attrval" }, a.value)
|
|
1007
|
+
))
|
|
1008
|
+
)
|
|
1009
|
+
),
|
|
1010
|
+
// ② 关系 — both directions from the 1-hop ego walk
|
|
1011
|
+
h("div", { className: "mneme-layer" },
|
|
1012
|
+
h("div", { className: "mneme-layerhead" },
|
|
1013
|
+
t("memory.entities.relations"),
|
|
1014
|
+
h("span", { className: "mneme-layercount" }, String(rootEdges.length))
|
|
1015
|
+
),
|
|
1016
|
+
rootEdges.length === 0
|
|
1017
|
+
? h("div", { className: "mneme-xempty" }, t("memory.settings.empty"))
|
|
1018
|
+
: rootEdges.map((e) => {
|
|
1019
|
+
const out = e.from === data.root.id;
|
|
1020
|
+
const other = nodeById.get(out ? e.to : e.from);
|
|
1021
|
+
const otherName = other?.name ?? (out ? e.to : e.from);
|
|
1022
|
+
return h("button", {
|
|
1023
|
+
key: e.id,
|
|
1024
|
+
className: "mneme-relrow",
|
|
1025
|
+
title: e.memory_id ? t("memory.graph.sourceMemory") : undefined,
|
|
1026
|
+
onClick: () => { if (e.memory_id && onJumpMemory) onJumpMemory({ id: e.memory_id }); }
|
|
1027
|
+
},
|
|
1028
|
+
h("span", { className: "mneme-relname", style: { textAlign: "right", flex: 1 } }, entity.name),
|
|
1029
|
+
h("span", { className: "mneme-reltype" }, e.relation_type),
|
|
1030
|
+
h("span", { className: "mneme-relname", style: { flex: 1 } }, otherName)
|
|
1031
|
+
);
|
|
1032
|
+
})
|
|
1033
|
+
),
|
|
1034
|
+
// ③ 关联记忆 — entity: prefix recall
|
|
1035
|
+
related.length > 0 && h("div", { className: "mneme-layer" },
|
|
1036
|
+
h("div", { className: "mneme-layerhead" },
|
|
1037
|
+
t("memory.graph.related"),
|
|
1038
|
+
h("span", { className: "mneme-layercount" }, String(related.length))
|
|
1039
|
+
),
|
|
1040
|
+
related.map((m) => h("button", {
|
|
1041
|
+
key: m.id,
|
|
1042
|
+
type: "button",
|
|
1043
|
+
className: "mneme-gs-link",
|
|
1044
|
+
title: t("memory.card.open"),
|
|
1045
|
+
onClick: () => onJumpMemory && onJumpMemory(m)
|
|
1046
|
+
}, m.title || m.content?.slice(0, 60)))
|
|
1047
|
+
),
|
|
1048
|
+
// ④ 关系图谱 — the ego graph
|
|
1049
|
+
h("div", { className: "mneme-layer" },
|
|
1050
|
+
h("div", { className: "mneme-layerhead" },
|
|
1051
|
+
t("memory.entities.graph"),
|
|
1052
|
+
h("span", { className: "mneme-layercount" },
|
|
1053
|
+
h("button", {
|
|
1054
|
+
className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1055
|
+
title: t("memory.graph.depth"),
|
|
1056
|
+
onClick: () => setDepth(depth === 1 ? 2 : 1)
|
|
1057
|
+
}, `${depth} ${t("memory.graph.depth")}`)),
|
|
1058
|
+
h("span", { className: "mneme-layercount" },
|
|
1059
|
+
h("button", {
|
|
1060
|
+
className: "mneme-chip",
|
|
1061
|
+
title: t("memory.graph.resetView"),
|
|
1062
|
+
onClick: resetView
|
|
1063
|
+
}, t("memory.graph.resetView")))
|
|
1064
|
+
),
|
|
1065
|
+
graphStatus === "empty"
|
|
1066
|
+
? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
|
|
1067
|
+
: h(react.Fragment, null,
|
|
1068
|
+
h("svg", {
|
|
1069
|
+
ref: svgRef,
|
|
1070
|
+
className: "mneme-graphsvg",
|
|
1071
|
+
viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
|
|
1072
|
+
style: { height: 320 },
|
|
1073
|
+
onMouseDown: onSurfaceMouseDown
|
|
1074
|
+
},
|
|
1075
|
+
h("g", {
|
|
1076
|
+
transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
|
|
1077
|
+
},
|
|
1078
|
+
data.edges.map((e) => {
|
|
1079
|
+
const a = nodeById.get(e.from), b = nodeById.get(e.to);
|
|
1080
|
+
if (!a || !b) return null;
|
|
1081
|
+
return h("line", {
|
|
1082
|
+
key: e.id,
|
|
1083
|
+
x1: a.x, y1: a.y, x2: b.x, y2: b.y,
|
|
1084
|
+
className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
|
|
1085
|
+
onClick: () => onEdgeClick(e)
|
|
1086
|
+
});
|
|
1087
|
+
}),
|
|
1088
|
+
nodes.map((n) => h("g", {
|
|
1089
|
+
key: n.id,
|
|
1090
|
+
className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
|
|
1091
|
+
transform: `translate(${n.x},${n.y})`,
|
|
1092
|
+
onMouseDown: (e) => onNodeMouseDown(e, n),
|
|
1093
|
+
onClick: () => onNodeClick(n),
|
|
1094
|
+
"data-node": n.name
|
|
1095
|
+
},
|
|
1096
|
+
h("circle", { r: nodeRadius(n), fill: typeColor(n.type) }),
|
|
1097
|
+
h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
|
|
1098
|
+
))
|
|
1099
|
+
)
|
|
1100
|
+
),
|
|
1101
|
+
h("div", { className: "mneme-graphhint" }, t("memory.graph.hint"))
|
|
1102
|
+
),
|
|
1103
|
+
side
|
|
1104
|
+
)
|
|
1105
|
+
)
|
|
1106
|
+
)
|
|
1051
1107
|
);
|
|
1108
|
+
|
|
1109
|
+
return h("div", { className: "mneme-ent" }, rail, detail);
|
|
1052
1110
|
}
|
|
1053
1111
|
|
|
1054
|
-
// --- Settings view: user profile, rules, custom commands, vector, token ---
|
|
1055
|
-
const styles = {
|
|
1056
|
-
footerButton: { padding: "4px 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "none", cursor: "pointer", fontSize: 12, margin: "2px 8px", color: "var(--dsw-alias-label-secondary, #666)", fontFamily: "inherit" }
|
|
1057
|
-
};
|
|
1058
1112
|
|
|
1059
1113
|
function SettingsContent({ t }) {
|
|
1060
1114
|
const [profile, setProfile] = react.useState("");
|
|
@@ -1072,10 +1126,21 @@ window.__ModuleLoader__.load({
|
|
|
1072
1126
|
(typeof window !== "undefined" && window.localStorage) ? window.localStorage.getItem("dsh-mneme-api-token") || "" : ""
|
|
1073
1127
|
);
|
|
1074
1128
|
const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const [
|
|
1078
|
-
const [
|
|
1129
|
+
// 运行模式 — light vs standard; null = still loading. The card keeps
|
|
1130
|
+
// its own busy/saved/error state so it never blocks the others.
|
|
1131
|
+
const [mode, setMode] = react.useState(null);
|
|
1132
|
+
const [modeBusy, setModeBusy] = react.useState(false);
|
|
1133
|
+
const [modeSaved, setModeSaved] = react.useState(false);
|
|
1134
|
+
const [modeError, setModeError] = react.useState("");
|
|
1135
|
+
// 外部访问 API — config fetched from the backend (the token is generated
|
|
1136
|
+
// and kept server-side); host/port inputs are drafts, PUT only on save.
|
|
1137
|
+
const [extapi, setExtapi] = react.useState(null);
|
|
1138
|
+
const [extapiHost, setExtapiHost] = react.useState("127.0.0.1");
|
|
1139
|
+
const [extapiPort, setExtapiPort] = react.useState("");
|
|
1140
|
+
const [extapiBusy, setExtapiBusy] = react.useState(false);
|
|
1141
|
+
const [extapiSaved, setExtapiSaved] = react.useState(false);
|
|
1142
|
+
const [extapiCopied, setExtapiCopied] = react.useState(false);
|
|
1143
|
+
const [extapiError, setExtapiError] = react.useState("");
|
|
1079
1144
|
|
|
1080
1145
|
const load = react.useCallback(async () => {
|
|
1081
1146
|
try {
|
|
@@ -1089,18 +1154,39 @@ window.__ModuleLoader__.load({
|
|
|
1089
1154
|
setRules(Array.isArray(r.rules) ? r.rules : []);
|
|
1090
1155
|
setCommands(Array.isArray(c.commands) ? c.commands : []);
|
|
1091
1156
|
setVector(v.config || { enabled: false, baseUrl: "", apiKey: "", model: "" });
|
|
1092
|
-
// autoTag loads independently so a config failure can't cascade-block the rest.
|
|
1093
|
-
try {
|
|
1094
|
-
const g = await apiFetch("/api/dsh-mneme/config").then((res) => res.json());
|
|
1095
|
-
setAutoTag(g.config?.autoTagEnabled === true);
|
|
1096
|
-
// issue #38: sidebar trigger defaults to visible (true).
|
|
1097
|
-
setShowSidebarTrigger(g.config?.showSidebarTrigger !== false);
|
|
1098
|
-
} catch { /* keep defaults */ }
|
|
1099
1157
|
} catch { /* ignore */ }
|
|
1100
1158
|
}, []);
|
|
1101
1159
|
|
|
1102
1160
|
react.useEffect(() => { load(); }, [load]);
|
|
1103
1161
|
|
|
1162
|
+
// Runtime mode + external API — two independent fetches: one failing
|
|
1163
|
+
// endpoint only errors its own card, never the other one.
|
|
1164
|
+
react.useEffect(() => {
|
|
1165
|
+
let cancelled = false;
|
|
1166
|
+
const toErr = (err) => (err && err.message) || "failed";
|
|
1167
|
+
apiFetch("/api/dsh-mneme/mode")
|
|
1168
|
+
.then((res) => { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); })
|
|
1169
|
+
.then((j) => { if (!cancelled) setMode(j.mode === "light" ? "light" : "standard"); })
|
|
1170
|
+
.catch((err) => { if (!cancelled) setModeError(toErr(err)); });
|
|
1171
|
+
apiFetch("/api/dsh-mneme/external-api")
|
|
1172
|
+
.then((res) => { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); })
|
|
1173
|
+
.then((j) => {
|
|
1174
|
+
if (cancelled) return;
|
|
1175
|
+
const cfg = (j && j.config) || {};
|
|
1176
|
+
const next = {
|
|
1177
|
+
enabled: !!cfg.enabled,
|
|
1178
|
+
host: cfg.host || "127.0.0.1",
|
|
1179
|
+
port: Number(cfg.port) || 0,
|
|
1180
|
+
token: cfg.token || ""
|
|
1181
|
+
};
|
|
1182
|
+
setExtapi(next);
|
|
1183
|
+
setExtapiHost(next.host);
|
|
1184
|
+
setExtapiPort(next.port ? String(next.port) : "");
|
|
1185
|
+
})
|
|
1186
|
+
.catch((err) => { if (!cancelled) setExtapiError(toErr(err)); });
|
|
1187
|
+
return () => { cancelled = true; };
|
|
1188
|
+
}, []);
|
|
1189
|
+
|
|
1104
1190
|
async function saveProfile() {
|
|
1105
1191
|
try {
|
|
1106
1192
|
await apiFetch("/api/dsh-mneme/profile", {
|
|
@@ -1192,31 +1278,87 @@ window.__ModuleLoader__.load({
|
|
|
1192
1278
|
} catch { /* ignore */ }
|
|
1193
1279
|
}
|
|
1194
1280
|
|
|
1195
|
-
|
|
1281
|
+
// Runtime mode: optimistic chip flip, rolled back on failure. Both
|
|
1282
|
+
// changes only take effect after a DSH restart — the saved hint says so.
|
|
1283
|
+
async function saveMode(next) {
|
|
1284
|
+
if (modeBusy) return;
|
|
1285
|
+
const prev = mode;
|
|
1286
|
+
setModeBusy(true);
|
|
1287
|
+
setModeError("");
|
|
1288
|
+
setMode(next);
|
|
1196
1289
|
try {
|
|
1197
|
-
await apiFetch("/api/dsh-mneme/
|
|
1290
|
+
const res = await apiFetch("/api/dsh-mneme/mode", {
|
|
1198
1291
|
method: "PUT",
|
|
1199
1292
|
headers: { "Content-Type": "application/json" },
|
|
1200
|
-
body: JSON.stringify({
|
|
1293
|
+
body: JSON.stringify({ mode: next })
|
|
1201
1294
|
});
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1295
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
1296
|
+
setModeSaved(true);
|
|
1297
|
+
setTimeout(() => setModeSaved(false), 2500);
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
setMode(prev);
|
|
1300
|
+
setModeError((err && err.message) || "failed");
|
|
1301
|
+
}
|
|
1302
|
+
setModeBusy(false);
|
|
1205
1303
|
}
|
|
1206
1304
|
|
|
1207
|
-
|
|
1305
|
+
// External API: the backend owns the token and returns the full config,
|
|
1306
|
+
// so every PUT response refreshes host/port/token from the server.
|
|
1307
|
+
async function putExtapi(body) {
|
|
1308
|
+
setExtapiBusy(true);
|
|
1309
|
+
setExtapiError("");
|
|
1208
1310
|
try {
|
|
1209
|
-
await apiFetch("/api/dsh-mneme/
|
|
1311
|
+
const res = await apiFetch("/api/dsh-mneme/external-api", {
|
|
1210
1312
|
method: "PUT",
|
|
1211
1313
|
headers: { "Content-Type": "application/json" },
|
|
1212
|
-
body: JSON.stringify(
|
|
1314
|
+
body: JSON.stringify(body)
|
|
1213
1315
|
});
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1316
|
+
const data = await res.json().catch(() => ({}));
|
|
1317
|
+
if (!res.ok) throw new Error(data.error || "HTTP " + res.status);
|
|
1318
|
+
const cfg = data.config || {};
|
|
1319
|
+
const next = {
|
|
1320
|
+
enabled: !!cfg.enabled,
|
|
1321
|
+
host: cfg.host || "127.0.0.1",
|
|
1322
|
+
port: Number(cfg.port) || 0,
|
|
1323
|
+
token: cfg.token || ""
|
|
1324
|
+
};
|
|
1325
|
+
setExtapi(next);
|
|
1326
|
+
setExtapiHost(next.host);
|
|
1327
|
+
setExtapiPort(next.port ? String(next.port) : "");
|
|
1328
|
+
setExtapiSaved(true);
|
|
1329
|
+
setTimeout(() => setExtapiSaved(false), 2500);
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
setExtapiError((err && err.message) || "failed");
|
|
1332
|
+
}
|
|
1333
|
+
setExtapiBusy(false);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function saveExtapiEnabled(enabled) {
|
|
1337
|
+
if (extapiBusy) return;
|
|
1338
|
+
putExtapi({ enabled });
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function saveExtapiAddress() {
|
|
1342
|
+
if (extapiBusy) return;
|
|
1343
|
+
const raw = String(extapiPort).trim();
|
|
1344
|
+
const port = Number(raw);
|
|
1345
|
+
if (!/^\d+$/.test(raw) || port < 1 || port > 65535) {
|
|
1346
|
+
setExtapiError(t("memory.settings.extapi.invalidPort"));
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
putExtapi({ port, host: extapiHost.trim() || "127.0.0.1" });
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function copyExtapiToken() {
|
|
1353
|
+
const token = extapi ? extapi.token || "" : "";
|
|
1354
|
+
navigator.clipboard?.writeText(token).then(
|
|
1355
|
+
() => { setExtapiCopied(true); setTimeout(() => setExtapiCopied(false), 1500); },
|
|
1356
|
+
() => {}
|
|
1357
|
+
);
|
|
1217
1358
|
}
|
|
1218
1359
|
|
|
1219
1360
|
return h("div", null,
|
|
1361
|
+
// 用户画像 — who the agent is talking to
|
|
1220
1362
|
h("section", { className: "mneme-set-sec" },
|
|
1221
1363
|
h("div", { className: "mneme-set-title" }, t("memory.settings.profile")),
|
|
1222
1364
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.profileHint")),
|
|
@@ -1231,6 +1373,7 @@ window.__ModuleLoader__.load({
|
|
|
1231
1373
|
saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved"))
|
|
1232
1374
|
)
|
|
1233
1375
|
),
|
|
1376
|
+
// 规则 — numbered rows, hover reveals the delete affordance
|
|
1234
1377
|
h("section", { className: "mneme-set-sec" },
|
|
1235
1378
|
h("div", { className: "mneme-set-title" }, t("memory.settings.rules")),
|
|
1236
1379
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.rulesHint")),
|
|
@@ -1259,6 +1402,7 @@ window.__ModuleLoader__.load({
|
|
|
1259
1402
|
h("button", { className: "mneme-btn", onClick: addRule }, t("memory.settings.ruleAdd"))
|
|
1260
1403
|
)
|
|
1261
1404
|
),
|
|
1405
|
+
// 自定义指令 — slash commands as titled rows
|
|
1262
1406
|
h("section", { className: "mneme-set-sec" },
|
|
1263
1407
|
h("div", { className: "mneme-set-title" }, t("memory.settings.commands")),
|
|
1264
1408
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.commandsHint")),
|
|
@@ -1289,30 +1433,87 @@ window.__ModuleLoader__.load({
|
|
|
1289
1433
|
)
|
|
1290
1434
|
)
|
|
1291
1435
|
),
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
h("
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
h(
|
|
1301
|
-
|
|
1302
|
-
|
|
1436
|
+
// 运行模式 — light vs standard chip radios; each click PUTs and the
|
|
1437
|
+
// change only lands after a DSH restart (green saved hint says so).
|
|
1438
|
+
h("section", { className: "mneme-set-card" },
|
|
1439
|
+
h("div", { className: "mneme-set-title" }, t("memory.settings.mode.title")),
|
|
1440
|
+
h("div", { className: "mneme-set-desc" }, t("memory.settings.mode.desc")),
|
|
1441
|
+
modeError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)", marginBottom: 8 } }, modeError),
|
|
1442
|
+
mode === null && !modeError
|
|
1443
|
+
? h("div", { className: "mneme-set-hint" }, "…")
|
|
1444
|
+
: h(react.Fragment, null,
|
|
1445
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
|
|
1446
|
+
h("button", {
|
|
1447
|
+
className: mode === "light" ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1448
|
+
disabled: modeBusy,
|
|
1449
|
+
onClick: () => saveMode("light")
|
|
1450
|
+
}, t("memory.settings.mode.light")),
|
|
1451
|
+
h("button", {
|
|
1452
|
+
className: mode === "standard" ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1453
|
+
disabled: modeBusy,
|
|
1454
|
+
onClick: () => saveMode("standard")
|
|
1455
|
+
}, t("memory.settings.mode.standard")),
|
|
1456
|
+
modeSaved && h("span", { className: "mneme-saved" }, t("memory.settings.mode.savedHint"))
|
|
1457
|
+
),
|
|
1458
|
+
mode === "light" && h("div", { className: "mneme-set-hint", style: { marginTop: 8 } },
|
|
1459
|
+
t("memory.settings.mode.offList"))
|
|
1460
|
+
)
|
|
1303
1461
|
),
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
),
|
|
1311
|
-
|
|
1312
|
-
h("
|
|
1313
|
-
|
|
1314
|
-
|
|
1462
|
+
// 外部访问 API — standalone HTTP service for plugins/CLI/desktop tools;
|
|
1463
|
+
// the token is generated and kept by the backend, so it is read-only
|
|
1464
|
+
// here with a copy affordance. Changes need a DSH restart.
|
|
1465
|
+
h("section", { className: "mneme-set-card" },
|
|
1466
|
+
h("div", { className: "mneme-set-title" }, t("memory.settings.extapi.title")),
|
|
1467
|
+
h("div", { className: "mneme-set-desc" }, t("memory.settings.extapi.desc")),
|
|
1468
|
+
extapiError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)", marginBottom: 8 } }, extapiError),
|
|
1469
|
+
extapi === null && !extapiError
|
|
1470
|
+
? h("div", { className: "mneme-set-hint" }, "…")
|
|
1471
|
+
: h(react.Fragment, null,
|
|
1472
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
|
|
1473
|
+
h("button", {
|
|
1474
|
+
className: extapi && extapi.enabled ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1475
|
+
disabled: extapiBusy,
|
|
1476
|
+
onClick: () => saveExtapiEnabled(true)
|
|
1477
|
+
}, t("memory.settings.extapi.enabled")),
|
|
1478
|
+
h("button", {
|
|
1479
|
+
className: extapi && !extapi.enabled ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1480
|
+
disabled: extapiBusy,
|
|
1481
|
+
onClick: () => saveExtapiEnabled(false)
|
|
1482
|
+
}, t("memory.settings.extapi.disabled")),
|
|
1483
|
+
extapiSaved && h("span", { className: "mneme-saved" }, t("memory.settings.extapi.savedHint"))
|
|
1484
|
+
),
|
|
1485
|
+
extapi && extapi.enabled && h(react.Fragment, null,
|
|
1486
|
+
h("div", { className: "mneme-set-hint", style: { marginTop: 10 } },
|
|
1487
|
+
`${t("memory.settings.extapi.address")}: http://${extapi.host}:${extapi.port}`),
|
|
1488
|
+
h("div", { style: { display: "flex", gap: 8, marginTop: 8 } },
|
|
1489
|
+
h("input", {
|
|
1490
|
+
className: "mneme-set-input",
|
|
1491
|
+
style: { marginBottom: 0, flex: 2, minWidth: 0, width: "auto" },
|
|
1492
|
+
value: extapiHost,
|
|
1493
|
+
placeholder: "127.0.0.1",
|
|
1494
|
+
onChange: (e) => setExtapiHost(e.target.value)
|
|
1495
|
+
}),
|
|
1496
|
+
h("input", {
|
|
1497
|
+
className: "mneme-set-input",
|
|
1498
|
+
style: { marginBottom: 0, flex: 1, minWidth: 0, width: "auto" },
|
|
1499
|
+
value: extapiPort,
|
|
1500
|
+
placeholder: t("memory.settings.extapi.port"),
|
|
1501
|
+
inputMode: "numeric",
|
|
1502
|
+
onChange: (e) => setExtapiPort(e.target.value)
|
|
1503
|
+
}),
|
|
1504
|
+
h("button", { className: "mneme-btn", disabled: extapiBusy, onClick: saveExtapiAddress },
|
|
1505
|
+
t("memory.settings.vectorSave"))
|
|
1506
|
+
),
|
|
1507
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 10 } },
|
|
1508
|
+
h("div", { className: "mneme-set-token", style: { flex: 1, minWidth: 0 } }, extapi.token || "—"),
|
|
1509
|
+
h("button", { className: "mneme-btn", onClick: copyExtapiToken },
|
|
1510
|
+
t("memory.settings.extapi.copy")),
|
|
1511
|
+
extapiCopied && h("span", { className: "mneme-saved" }, t("memory.settings.extapi.copied"))
|
|
1512
|
+
)
|
|
1513
|
+
)
|
|
1514
|
+
)
|
|
1315
1515
|
),
|
|
1516
|
+
// 向量搜索 — semantic recall over an embeddings API
|
|
1316
1517
|
h("section", { className: "mneme-set-sec" },
|
|
1317
1518
|
h("div", { className: "mneme-set-title" }, t("memory.settings.vectorTitle")),
|
|
1318
1519
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.vectorHint")),
|
|
@@ -1330,6 +1531,7 @@ window.__ModuleLoader__.load({
|
|
|
1330
1531
|
reindexMsg && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #666)" } }, reindexMsg)
|
|
1331
1532
|
)
|
|
1332
1533
|
),
|
|
1534
|
+
// API Token — advanced, last
|
|
1333
1535
|
h("section", { className: "mneme-set-sec" },
|
|
1334
1536
|
h("div", { className: "mneme-set-title" }, t("memory.settings.apiTokenTitle")),
|
|
1335
1537
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.apiTokenHint")),
|
|
@@ -1377,9 +1579,6 @@ window.__ModuleLoader__.load({
|
|
|
1377
1579
|
return matches;
|
|
1378
1580
|
}
|
|
1379
1581
|
|
|
1380
|
-
// Click the memory library tab and wait (bounded, rAF-polled) until the
|
|
1381
|
-
// host actually marks it selected — a silent React re-render gap must not
|
|
1382
|
-
// be mistaken for success, or the fallback would never kick in.
|
|
1383
1582
|
// Click the memory library tab and wait (bounded, rAF-polled) until the
|
|
1384
1583
|
// host marks it selected AND our explorer actually rendered (`.mneme-x`).
|
|
1385
1584
|
// aria-selected alone cannot tell our tab from a same-labelled tab of
|
|
@@ -1454,314 +1653,179 @@ window.__ModuleLoader__.load({
|
|
|
1454
1653
|
return tree;
|
|
1455
1654
|
}
|
|
1456
1655
|
|
|
1457
|
-
|
|
1458
|
-
//
|
|
1459
|
-
|
|
1460
|
-
//
|
|
1461
|
-
//
|
|
1462
|
-
//
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1656
|
+
const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history"];
|
|
1657
|
+
const PAGE_SIZE = 100; // browse page size — the tree grows by 100-row pages
|
|
1658
|
+
|
|
1659
|
+
// --- Status sub-view: a responsive grid of stat cards. Every card owns
|
|
1660
|
+
// its fetch, loading ("…") and error state, so one failing endpoint
|
|
1661
|
+
// never blanks or blocks the others. ---
|
|
1662
|
+
function StatusCard({ t, title, loading, error, num, cap }) {
|
|
1663
|
+
return h("div", { className: "mneme-statuscard" },
|
|
1664
|
+
h("div", { className: "mneme-xcolhead" }, title),
|
|
1665
|
+
loading
|
|
1666
|
+
? h("div", { className: "mneme-statusnum" }, "…")
|
|
1667
|
+
: error
|
|
1668
|
+
? h("div", { className: "mneme-statuscap", style: { color: "var(--dsw-alias-state-error,#c33)" } }, t("memory.status.error"))
|
|
1669
|
+
: h(react.Fragment, null,
|
|
1670
|
+
h("div", { className: "mneme-statusnum" }, num),
|
|
1671
|
+
cap ? h("div", { className: "mneme-statuscap" }, cap) : null
|
|
1672
|
+
)
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1471
1675
|
|
|
1676
|
+
// 记忆总数 — list total plus per-type subtotals; each subtotal request
|
|
1677
|
+
// may fail independently (rendered as "—") without sinking the card.
|
|
1678
|
+
function MemoriesStatusCard({ t }) {
|
|
1679
|
+
const [state, setState] = useState({ loading: true, error: false, total: 0, byType: [] });
|
|
1472
1680
|
useEffect(() => {
|
|
1473
1681
|
let cancelled = false;
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1481
|
-
|
|
1682
|
+
const one = (ty) =>
|
|
1683
|
+
apiFetch(`/api/dsh-mneme/list?limit=1&order=chrono${ty ? `&type=${encodeURIComponent(ty)}` : ""}`)
|
|
1684
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); });
|
|
1685
|
+
one()
|
|
1686
|
+
.then((all) => {
|
|
1687
|
+
if (cancelled) return null;
|
|
1688
|
+
const total = all.total || 0;
|
|
1689
|
+
return Promise.all(EXPLORER_TYPES.map((ty) =>
|
|
1690
|
+
one(ty).then((j) => [ty, j.total || 0]).catch(() => [ty, null])
|
|
1691
|
+
)).then((byType) => {
|
|
1692
|
+
if (!cancelled) setState({ loading: false, error: false, total, byType });
|
|
1693
|
+
});
|
|
1482
1694
|
})
|
|
1483
|
-
.catch(() => { if (!cancelled) {
|
|
1484
|
-
apiFetch("/api/dsh-mneme/config")
|
|
1485
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
1486
|
-
.then((d) => { if (!cancelled && d?.config && typeof d.config.manualTagEnabled === "boolean") setEditMode(d.config.manualTagEnabled); })
|
|
1487
|
-
.catch(() => {});
|
|
1695
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, byType: [] }); });
|
|
1488
1696
|
return () => { cancelled = true; };
|
|
1489
1697
|
}, []);
|
|
1698
|
+
const cap = state.byType
|
|
1699
|
+
.map(([ty, n]) => `${typeLabel(t, ty)} ${n === null ? "—" : n}`)
|
|
1700
|
+
.join(" · ");
|
|
1701
|
+
return h(StatusCard, {
|
|
1702
|
+
t,
|
|
1703
|
+
title: t("memory.status.memories"),
|
|
1704
|
+
loading: state.loading,
|
|
1705
|
+
error: state.error,
|
|
1706
|
+
num: state.total.toLocaleString(),
|
|
1707
|
+
cap
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1490
1710
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
const handleDelete = (memId, e) => {
|
|
1508
|
-
e?.stopPropagation?.();
|
|
1509
|
-
// 第一击进入「确认?」态,第二击才真正发 DELETE;宿主 web 环境里
|
|
1510
|
-
// window.confirm 不可靠(可能被拦截或直接返回 false),点击会无反应,
|
|
1511
|
-
// 所以改为面板内联两步确认,不依赖任何原生对话框。
|
|
1512
|
-
if (confirmingId !== memId) {
|
|
1513
|
-
setConfirmingId(memId);
|
|
1514
|
-
clearTimeout(confirmTimer.current);
|
|
1515
|
-
confirmTimer.current = setTimeout(() => setConfirmingId((cur) => (cur === memId ? null : cur)), 3000);
|
|
1516
|
-
return;
|
|
1517
|
-
}
|
|
1518
|
-
clearTimeout(confirmTimer.current);
|
|
1519
|
-
setConfirmingId(null);
|
|
1520
|
-
apiFetch("/api/dsh-mneme/memories", {
|
|
1521
|
-
method: "DELETE",
|
|
1522
|
-
headers: { "Content-Type": "application/json" },
|
|
1523
|
-
body: JSON.stringify({ id: memId })
|
|
1524
|
-
})
|
|
1525
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
1526
|
-
.then((d) => {
|
|
1527
|
-
if (!d || d.deleted !== true) {
|
|
1528
|
-
// 服务端未确认删除也给出可见提示,不静默吞掉
|
|
1529
|
-
setDelErrorId(memId);
|
|
1530
|
-
setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
|
|
1531
|
-
return;
|
|
1711
|
+
// 实体 — directory snapshot grouped by entity type.
|
|
1712
|
+
function EntitiesStatusCard({ t }) {
|
|
1713
|
+
const [state, setState] = useState({ loading: true, error: false, total: 0, byType: [] });
|
|
1714
|
+
useEffect(() => {
|
|
1715
|
+
let cancelled = false;
|
|
1716
|
+
const order = ["organization", "person", "project", "technology", "concept"];
|
|
1717
|
+
apiFetch("/api/dsh-mneme/entities?limit=500")
|
|
1718
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
1719
|
+
.then((j) => {
|
|
1720
|
+
if (cancelled) return;
|
|
1721
|
+
const list = Array.isArray(j.entities) ? j.entities : [];
|
|
1722
|
+
const counts = new Map();
|
|
1723
|
+
for (const e of list) {
|
|
1724
|
+
const key = order.includes(e.type) ? e.type : "other";
|
|
1725
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
1532
1726
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
})
|
|
1727
|
+
const byType = order.concat("other")
|
|
1728
|
+
.filter((k) => counts.has(k))
|
|
1729
|
+
.map((k) => [k, counts.get(k)]);
|
|
1730
|
+
setState({ loading: false, error: false, total: list.length, byType });
|
|
1537
1731
|
})
|
|
1538
|
-
.catch(() => {
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
const err = delErrorId === m.id;
|
|
1551
|
-
return h("div", {
|
|
1552
|
-
key: m.id,
|
|
1553
|
-
className: err ? "mneme-dir-item mneme-dir-error" : "mneme-dir-item",
|
|
1554
|
-
// 点击行内其它地方取消「确认?」态
|
|
1555
|
-
onClick: resetConfirm
|
|
1556
|
-
},
|
|
1557
|
-
h("span", { className: "mneme-tree-line" }),
|
|
1558
|
-
h("button", { type: "button", className: "mneme-dir-ititle", onClick: () => onJump(m) }, m.title || m.content?.slice(0, 40)),
|
|
1559
|
-
h("span", { className: "mneme-dir-itime" }, formatDate(m.updated_at || m.created_at)),
|
|
1560
|
-
editMode && h("button", {
|
|
1561
|
-
type: "button",
|
|
1562
|
-
className: confirming ? "mneme-dir-del mneme-dir-del-confirm" : "mneme-dir-del",
|
|
1563
|
-
"aria-label": "delete",
|
|
1564
|
-
onClick: (e) => handleDelete(m.id, e)
|
|
1565
|
-
}, confirming ? t("directory.deleteConfirmShort") : "✕"),
|
|
1566
|
-
err && h("span", { className: "mneme-dir-del-err" }, t("directory.deleteError"))
|
|
1567
|
-
);
|
|
1568
|
-
});
|
|
1569
|
-
return h("div", {
|
|
1570
|
-
key: folderKey,
|
|
1571
|
-
className: "mneme-dir-folder",
|
|
1572
|
-
"data-expanded": String(open)
|
|
1573
|
-
},
|
|
1574
|
-
h("button", {
|
|
1575
|
-
type: "button",
|
|
1576
|
-
className: "mneme-dir-folderhead",
|
|
1577
|
-
"aria-expanded": String(open),
|
|
1578
|
-
onClick: () => toggle(folderKey)
|
|
1579
|
-
},
|
|
1580
|
-
h("span", { className: "mneme-dir-caret" }, open ? "▾" : "▸"),
|
|
1581
|
-
h("span", { className: "mneme-dir-fname" }, `📁 ${name}`),
|
|
1582
|
-
h("span", { className: "mneme-dir-fcount" }, String(memories.length))
|
|
1583
|
-
),
|
|
1584
|
-
open && h("div", { className: "mneme-dir-fbody" },
|
|
1585
|
-
rows.length > 0 ? rows : h("div", { className: "mneme-dir-empty" }, emptyLabel || t("memory.directory.empty"))
|
|
1586
|
-
)
|
|
1587
|
-
);
|
|
1588
|
-
};
|
|
1589
|
-
|
|
1590
|
-
if (status === "loading") {
|
|
1591
|
-
return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.loading")));
|
|
1592
|
-
}
|
|
1593
|
-
const hasAny = dir && (dir.groups.length > 0 || dir.untagged.length > 0);
|
|
1594
|
-
if (status === "error" || !hasAny) {
|
|
1595
|
-
return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.empty")));
|
|
1596
|
-
}
|
|
1597
|
-
return h("div", { className: editMode ? "mneme-directory mneme-edit-on" : "mneme-directory" },
|
|
1598
|
-
h("label", { className: "mneme-edit-toggle" },
|
|
1599
|
-
h("input", { type: "checkbox", checked: editMode, onChange: toggleEdit }),
|
|
1600
|
-
t("directory.editToggle")
|
|
1601
|
-
),
|
|
1602
|
-
dir.groups.map((g) => renderFolder(g.tag, g.memories)),
|
|
1603
|
-
dir.untagged.length > 0 && renderFolder(t("memory.directory.untagged"), dir.untagged, "mneme-untagged")
|
|
1604
|
-
);
|
|
1732
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, byType: [] }); });
|
|
1733
|
+
return () => { cancelled = true; };
|
|
1734
|
+
}, []);
|
|
1735
|
+
const cap = state.byType.map(([ty, n]) => `${entityTypeLabel(t, ty)} ${n}`).join(" · ");
|
|
1736
|
+
return h(StatusCard, {
|
|
1737
|
+
t,
|
|
1738
|
+
title: t("memory.status.entities"),
|
|
1739
|
+
loading: state.loading,
|
|
1740
|
+
error: state.error,
|
|
1741
|
+
num: state.total.toLocaleString(),
|
|
1742
|
+
cap
|
|
1743
|
+
});
|
|
1605
1744
|
}
|
|
1606
1745
|
|
|
1607
|
-
//
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
// bar chart and a 7-day creation trend. Data comes from /stats; each tile
|
|
1611
|
-
// probes /list?type=X&limit=1 for a representative latest title (the list
|
|
1612
|
-
// API sorts importance-first, so limit=1 is the type's most prominent row).
|
|
1613
|
-
const OVERVIEW_TYPE_ORDER = ["preference", "project", "decision", "history", "user", "fact", "summary", "pattern"];
|
|
1614
|
-
// Injection-priority tiers from service.js (summary > preference/user >
|
|
1615
|
-
// importance): these layers ride into nearly every prompt.
|
|
1616
|
-
const OVERVIEW_INJECT_TYPES = ["summary", "user", "preference"];
|
|
1617
|
-
|
|
1618
|
-
function OverviewPanel({ t, onPickType }) {
|
|
1619
|
-
const [stats, setStats] = useState(null); // null = loading
|
|
1620
|
-
const [profile, setProfile] = useState(null); // user-type memories (list card)
|
|
1621
|
-
const [latest, setLatest] = useState({}); // type -> representative memory | null
|
|
1622
|
-
|
|
1746
|
+
// 向量索引 — whether semantic recall is switched on.
|
|
1747
|
+
function VectorStatusCard({ t }) {
|
|
1748
|
+
const [state, setState] = useState({ loading: true, error: false, enabled: false });
|
|
1623
1749
|
useEffect(() => {
|
|
1624
1750
|
let cancelled = false;
|
|
1625
|
-
apiFetch("/api/dsh-mneme/
|
|
1626
|
-
.then((
|
|
1627
|
-
.then((
|
|
1628
|
-
.catch(() => { if (!cancelled)
|
|
1629
|
-
apiFetch("/api/dsh-mneme/list?type=user&limit=20")
|
|
1630
|
-
.then((r) => (r.ok ? r.json() : { items: [] }))
|
|
1631
|
-
.then((d) => { if (!cancelled) setProfile(Array.isArray(d.items) ? d.items : []); })
|
|
1632
|
-
.catch(() => { if (!cancelled) setProfile([]); });
|
|
1751
|
+
apiFetch("/api/dsh-mneme/vector-config")
|
|
1752
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
1753
|
+
.then((j) => { if (!cancelled) setState({ loading: false, error: false, enabled: !!j.config?.enabled }); })
|
|
1754
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, enabled: false }); });
|
|
1633
1755
|
return () => { cancelled = true; };
|
|
1634
1756
|
}, []);
|
|
1757
|
+
return h(StatusCard, {
|
|
1758
|
+
t,
|
|
1759
|
+
title: t("memory.status.vector"),
|
|
1760
|
+
loading: state.loading,
|
|
1761
|
+
error: state.error,
|
|
1762
|
+
num: state.enabled ? t("memory.status.vectorOn") : t("memory.status.vectorOff"),
|
|
1763
|
+
cap: ""
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1635
1766
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
Object.keys(byType).filter((k) => !OVERVIEW_TYPE_ORDER.includes(k)).sort())
|
|
1640
|
-
: [];
|
|
1641
|
-
|
|
1642
|
-
// Representative-latest probe, one small request per visible layer.
|
|
1643
|
-
// Derived purely from stats, so [stats] is the full dependency set.
|
|
1767
|
+
// LLM 消耗 — calls + tokens over the trailing 7 days.
|
|
1768
|
+
function LlmStatusCard({ t }) {
|
|
1769
|
+
const [state, setState] = useState({ loading: true, error: false, calls: 0, tokens: 0 });
|
|
1644
1770
|
useEffect(() => {
|
|
1645
|
-
if (!stats || gridTypes.length === 0) return;
|
|
1646
1771
|
let cancelled = false;
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
.
|
|
1652
|
-
|
|
1772
|
+
apiFetch("/api/dsh-mneme/semantic/llm-audit/stats?days=7")
|
|
1773
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
1774
|
+
.then((j) => {
|
|
1775
|
+
if (cancelled) return;
|
|
1776
|
+
const s = j && typeof j === "object" ? (j.stats || j) : {};
|
|
1777
|
+
setState({ loading: false, error: false, calls: Number(s.total_calls ?? 0), tokens: Number(s.total_tokens ?? 0) });
|
|
1778
|
+
})
|
|
1779
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, calls: 0, tokens: 0 }); });
|
|
1653
1780
|
return () => { cancelled = true; };
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
h("span", { className: "mneme-ov-distlabel" }, typeLabel(t, key)),
|
|
1665
|
-
h("span", { className: "mneme-ov-distcount" }, String(count)),
|
|
1666
|
-
h("div", { className: "mneme-ov-disttrack" },
|
|
1667
|
-
h("div", { className: "mneme-ov-distbar", style: { width: `${pct}%` } }))
|
|
1668
|
-
);
|
|
1669
|
-
};
|
|
1670
|
-
|
|
1671
|
-
const tile = (key) => {
|
|
1672
|
-
const inject = OVERVIEW_INJECT_TYPES.includes(key);
|
|
1673
|
-
const last = latest[key];
|
|
1674
|
-
return h("button", {
|
|
1675
|
-
key,
|
|
1676
|
-
type: "button",
|
|
1677
|
-
className: "mneme-card mneme-ov-tile",
|
|
1678
|
-
title: t("memory.card.open"),
|
|
1679
|
-
onClick: () => onPickType && onPickType(key)
|
|
1680
|
-
},
|
|
1681
|
-
h("div", { className: "mneme-ov-tilehead" },
|
|
1682
|
-
h("span", { className: "mneme-ov-tiletitle" }, typeLabel(t, key)),
|
|
1683
|
-
h("span", { className: inject ? "mneme-ov-badge" : "mneme-ov-badge mneme-ov-badge--dim" },
|
|
1684
|
-
inject ? t("memory.overview.badgeInject") : t("memory.overview.badgeImportance")),
|
|
1685
|
-
h("span", { className: "mneme-ov-tilecount" }, String(byType[key] || 0))
|
|
1686
|
-
),
|
|
1687
|
-
h("div", { className: "mneme-ov-tilelatest" },
|
|
1688
|
-
(last && (last.title || (last.content || "").slice(0, 40))) || t("memory.overview.none"))
|
|
1689
|
-
);
|
|
1690
|
-
};
|
|
1691
|
-
|
|
1692
|
-
// Trend bars scale to the busiest day; heights are fixed px so they do
|
|
1693
|
-
// not depend on percentage resolution inside the flex columns.
|
|
1694
|
-
const recent = Array.isArray(stats.recent) ? stats.recent : [];
|
|
1695
|
-
const maxTrend = Math.max(1, ...recent.map((d) => d.count || 0));
|
|
1696
|
-
const trendCol = (d) => {
|
|
1697
|
-
const count = d.count || 0;
|
|
1698
|
-
const px = count > 0 ? 6 + Math.round((count / maxTrend) * 90) : 0;
|
|
1699
|
-
return h("div", {
|
|
1700
|
-
key: d.date,
|
|
1701
|
-
className: "mneme-ov-trendcol",
|
|
1702
|
-
title: `${d.date} · ${t("memory.explorer.count").replace("{n}", String(count))}`
|
|
1703
|
-
},
|
|
1704
|
-
h("div", { className: "mneme-ov-trendbar", style: { height: `${px}px` } }),
|
|
1705
|
-
h("div", { className: "mneme-ov-trenddate" }, (d.date || "").slice(5)) // MM-DD
|
|
1706
|
-
);
|
|
1707
|
-
};
|
|
1781
|
+
}, []);
|
|
1782
|
+
return h(StatusCard, {
|
|
1783
|
+
t,
|
|
1784
|
+
title: t("memory.status.llm"),
|
|
1785
|
+
loading: state.loading,
|
|
1786
|
+
error: state.error,
|
|
1787
|
+
num: state.tokens.toLocaleString(),
|
|
1788
|
+
cap: t("memory.status.llmCalls").replace("{n}", state.calls.toLocaleString())
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1708
1791
|
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
: profile.map((m) => h("div", { key: m.id, className: "mneme-ov-prow" },
|
|
1717
|
-
h("span", { className: "mneme-ov-ptitle" }, m.title || "—"),
|
|
1718
|
-
h("span", { className: "mneme-ov-ppreview" }, (m.content_preview || m.content || "").slice(0, 100))
|
|
1719
|
-
))
|
|
1720
|
-
),
|
|
1721
|
-
h("div", { className: "mneme-card", role: "region", "aria-label": t("memory.overview.distribution") },
|
|
1722
|
-
h("div", { className: "mneme-xcolhead" }, t("memory.overview.distribution")),
|
|
1723
|
-
h("div", { className: "mneme-ov-dist" }, gridTypes.map(distRow))
|
|
1724
|
-
),
|
|
1725
|
-
h("div", { className: "mneme-xcolhead" }, t("memory.overview.layers")),
|
|
1726
|
-
h("div", { className: "mneme-ov-grid" }, gridTypes.map(tile)),
|
|
1727
|
-
h("div", { className: "mneme-card", role: "region", "aria-label": t("memory.overview.trend") },
|
|
1728
|
-
h("div", { className: "mneme-xcolhead" }, t("memory.overview.trend")),
|
|
1729
|
-
recent.length === 0
|
|
1730
|
-
? h("div", { className: "mneme-ov-tilelatest" }, t("memory.overview.none"))
|
|
1731
|
-
: h("div", { className: "mneme-ov-trend" }, recent.map(trendCol))
|
|
1792
|
+
function StatusPanel({ t }) {
|
|
1793
|
+
return h("div", { className: "mneme-status" },
|
|
1794
|
+
h("div", { className: "mneme-statusgrid" },
|
|
1795
|
+
h(MemoriesStatusCard, { t }),
|
|
1796
|
+
h(EntitiesStatusCard, { t }),
|
|
1797
|
+
h(VectorStatusCard, { t }),
|
|
1798
|
+
h(LlmStatusCard, { t })
|
|
1732
1799
|
)
|
|
1733
1800
|
);
|
|
1734
1801
|
}
|
|
1735
1802
|
|
|
1736
|
-
const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history", "user", "fact"];
|
|
1737
1803
|
|
|
1738
1804
|
function MemoryExplorer({ t }) {
|
|
1739
|
-
const [view, setView] = useState("memory"); // memory |
|
|
1740
|
-
const [items, setItems] = useState([]);
|
|
1805
|
+
const [view, setView] = useState("memory"); // memory | entity | status | settings
|
|
1806
|
+
const [items, setItems] = useState([]); // loaded browse pages, chrono desc
|
|
1807
|
+
const [total, setTotal] = useState(0); // server count for the current type filter
|
|
1741
1808
|
const [loading, setLoading] = useState(true);
|
|
1809
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
1742
1810
|
const [type, setType] = useState("all");
|
|
1811
|
+
const [minImp, setMinImp] = useState(0); // importance floor: 0 = all, else 3/4/5
|
|
1743
1812
|
const [query, setQuery] = useState("");
|
|
1744
1813
|
const [semantic, setSemantic] = useState(false);
|
|
1745
1814
|
const [vecEnabled, setVecEnabled] = useState(false);
|
|
1746
1815
|
const [searchTopK, setSearchTopK] = useState(20);
|
|
1747
1816
|
const [remoteItems, setRemoteItems] = useState(null);
|
|
1748
1817
|
const [selectedId, setSelectedId] = useState(null);
|
|
1818
|
+
const [expandedMonths, setExpandedMonths] = useState(null); // null = 仅最新一个月展开
|
|
1749
1819
|
const [collapsed, setCollapsed] = useState({});
|
|
1750
|
-
// v0.6.3 directory sub-view: its own folder-collapse state, kept in
|
|
1751
|
-
// MemoryExplorer so switching tabs does not lose it and jumpToMemory's
|
|
1752
|
-
// time-tree reset (setCollapsed({})) cannot clobber it.
|
|
1753
|
-
const [dirCollapsed, setDirCollapsed] = useState({});
|
|
1754
1820
|
const [copied, setCopied] = useState(false);
|
|
1755
1821
|
const [reloadKey, setReloadKey] = useState(0);
|
|
1822
|
+
const [confirmDelete, setConfirmDelete] = useState(false); // two-step delete armed
|
|
1823
|
+
const [deleting, setDeleting] = useState(false);
|
|
1824
|
+
const [deleteError, setDeleteError] = useState(false);
|
|
1825
|
+
const [deleteMsg, setDeleteMsg] = useState(false); // transient "deleted" notice
|
|
1756
1826
|
const [graphFocus, setGraphFocus] = useState("");
|
|
1757
|
-
// v0.6.2 tag editing state for the detail pane. The authoritative tag set
|
|
1758
|
-
// lives server-side (entity_attrs), so it is fetched per selected memory
|
|
1759
|
-
// and refreshed after every write instead of trusting the list snapshot.
|
|
1760
|
-
const [tagTags, setTagTags] = useState([]);
|
|
1761
|
-
const [tagManual, setTagManual] = useState(true);
|
|
1762
|
-
const [tagInput, setTagInput] = useState("");
|
|
1763
|
-
const [tagAdding, setTagAdding] = useState(false);
|
|
1764
1827
|
const itemRefs = useRef(new Map());
|
|
1828
|
+
const moreRef = useRef(null);
|
|
1765
1829
|
|
|
1766
1830
|
useEffect(() => {
|
|
1767
1831
|
apiFetch("/api/dsh-mneme/vector-config")
|
|
@@ -1770,60 +1834,112 @@ window.__ModuleLoader__.load({
|
|
|
1770
1834
|
.catch(() => {});
|
|
1771
1835
|
}, []);
|
|
1772
1836
|
|
|
1837
|
+
// One query string behind every browse-list fetch (initial page,
|
|
1838
|
+
// loadMore, auto-refresh): the type filter and the importance floor
|
|
1839
|
+
// always travel together, so counts and pagination stay consistent.
|
|
1840
|
+
const filterQS = (type === "all" ? "" : `&type=${encodeURIComponent(type)}`)
|
|
1841
|
+
+ (minImp ? `&minImportance=${minImp}` : "");
|
|
1842
|
+
const listUrl = (offset) => `/api/dsh-mneme/list?limit=${PAGE_SIZE}&offset=${offset}${filterQS}&order=chrono`;
|
|
1843
|
+
|
|
1844
|
+
// Browse = paged, pure-chronological pages (order=chrono). The default
|
|
1845
|
+
// importance ordering would interleave months across pages and break
|
|
1846
|
+
// the month tree; type filters server-side so pages stay consistent
|
|
1847
|
+
// however large the store grows.
|
|
1773
1848
|
useEffect(() => {
|
|
1774
1849
|
let cancelled = false;
|
|
1775
1850
|
setLoading(true);
|
|
1776
|
-
apiFetch(
|
|
1777
|
-
.then((res) => (res.ok ? res.json() : { items: [] }))
|
|
1778
|
-
.then((d) => {
|
|
1779
|
-
|
|
1851
|
+
apiFetch(listUrl(0))
|
|
1852
|
+
.then((res) => (res.ok ? res.json() : { items: [], total: 0 }))
|
|
1853
|
+
.then((d) => {
|
|
1854
|
+
if (cancelled) return;
|
|
1855
|
+
setItems(d.items || []);
|
|
1856
|
+
setTotal(d.total || 0);
|
|
1857
|
+
setExpandedMonths(null); // 新数据回到「仅最新月展开」
|
|
1858
|
+
setLoading(false);
|
|
1859
|
+
})
|
|
1860
|
+
.catch(() => { if (!cancelled) { setItems([]); setTotal(0); setLoading(false); } });
|
|
1780
1861
|
return () => { cancelled = true; };
|
|
1781
|
-
}, [reloadKey]);
|
|
1862
|
+
}, [reloadKey, filterQS]);
|
|
1863
|
+
|
|
1864
|
+
const canLoadMore = !query.trim() && !remoteItems && items.length < total;
|
|
1865
|
+
const loadMore = useCallback(() => {
|
|
1866
|
+
if (loadingMore || query.trim() || remoteItems) return;
|
|
1867
|
+
if (items.length >= total) return;
|
|
1868
|
+
setLoadingMore(true);
|
|
1869
|
+
apiFetch(listUrl(items.length))
|
|
1870
|
+
.then((res) => (res.ok ? res.json() : { items: [] }))
|
|
1871
|
+
.then((d) => setItems((cur) => cur.concat(d.items || [])))
|
|
1872
|
+
.catch(() => {})
|
|
1873
|
+
.finally(() => setLoadingMore(false));
|
|
1874
|
+
}, [items.length, total, loadingMore, query, remoteItems, filterQS]);
|
|
1782
1875
|
|
|
1783
|
-
//
|
|
1784
|
-
// while
|
|
1785
|
-
|
|
1786
|
-
|
|
1876
|
+
// Infinite scroll: a sentinel just past the loaded rows pulls the next
|
|
1877
|
+
// page while browsing (search results arrive complete already).
|
|
1878
|
+
useEffect(() => {
|
|
1879
|
+
const el = moreRef.current;
|
|
1880
|
+
if (!el || !canLoadMore) return undefined;
|
|
1881
|
+
const io = new IntersectionObserver((entries) => {
|
|
1882
|
+
if (entries.some((e) => e.isIntersecting)) loadMore();
|
|
1883
|
+
}, { root: el.closest(".mneme-xtree"), rootMargin: "240px" });
|
|
1884
|
+
io.observe(el);
|
|
1885
|
+
return () => io.disconnect();
|
|
1886
|
+
}, [canLoadMore, loadMore]);
|
|
1887
|
+
|
|
1888
|
+
// Search hits the server so matches are global — not limited to the
|
|
1889
|
+
// loaded pages: keyword = literal text; vector = semantic ranking
|
|
1890
|
+
// (the pipeline falls back to keyword when no embedder is available).
|
|
1787
1891
|
useEffect(() => {
|
|
1788
|
-
const
|
|
1789
|
-
if (
|
|
1892
|
+
const query0 = query.trim();
|
|
1893
|
+
if (!query0 || query0.startsWith("entity:")) { setRemoteItems(null); return; }
|
|
1790
1894
|
let cancelled = false;
|
|
1791
1895
|
const timer = setTimeout(() => {
|
|
1792
|
-
|
|
1896
|
+
const mode = semantic ? "vector" : "keyword";
|
|
1897
|
+
apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent(query0)}&mode=${mode}&topK=${searchTopK}`)
|
|
1793
1898
|
.then((res) => (res.ok ? res.json() : { items: [] }))
|
|
1794
1899
|
.then((d) => { if (!cancelled) setRemoteItems(d.items || []); })
|
|
1795
1900
|
.catch(() => { if (!cancelled) setRemoteItems([]); });
|
|
1796
1901
|
}, 250);
|
|
1797
1902
|
return () => { cancelled = true; clearTimeout(timer); };
|
|
1798
|
-
}, [
|
|
1903
|
+
}, [query, semantic, searchTopK]);
|
|
1904
|
+
|
|
1905
|
+
// Live view: while the memory tab is open, quietly re-fetch page 1
|
|
1906
|
+
// every 30s (and on every activation) so fresh memories surface
|
|
1907
|
+
// without a manual refresh. Later pages stay as loaded.
|
|
1908
|
+
const refreshFirstPage = useCallback(() => {
|
|
1909
|
+
if (query.trim() || remoteItems) return;
|
|
1910
|
+
apiFetch(listUrl(0))
|
|
1911
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1912
|
+
.then((d) => {
|
|
1913
|
+
if (!d) return;
|
|
1914
|
+
setTotal(d.total || 0);
|
|
1915
|
+
setItems((cur) => {
|
|
1916
|
+
const fresh = d.items || [];
|
|
1917
|
+
const seen = new Set(fresh.map((m) => m.id));
|
|
1918
|
+
return fresh.concat(cur.filter((m) => !seen.has(m.id)));
|
|
1919
|
+
});
|
|
1920
|
+
})
|
|
1921
|
+
.catch(() => {});
|
|
1922
|
+
}, [query, remoteItems, filterQS]);
|
|
1923
|
+
|
|
1924
|
+
useEffect(() => {
|
|
1925
|
+
if (view !== "memory") return undefined;
|
|
1926
|
+
refreshFirstPage();
|
|
1927
|
+
const iv = setInterval(() => {
|
|
1928
|
+
if (document.visibilityState === "visible") refreshFirstPage();
|
|
1929
|
+
}, 30000);
|
|
1930
|
+
return () => clearInterval(iv);
|
|
1931
|
+
}, [view, refreshFirstPage]);
|
|
1799
1932
|
|
|
1800
1933
|
useEffect(() => {
|
|
1801
1934
|
if (!selectedId) return;
|
|
1802
1935
|
itemRefs.current.get(selectedId)?.scrollIntoView({ block: "nearest" });
|
|
1803
1936
|
}, [selectedId]);
|
|
1804
1937
|
|
|
1805
|
-
//
|
|
1806
|
-
//
|
|
1807
|
-
const loadTags = (id) => {
|
|
1808
|
-
if (!id) { setTagTags([]); setTagManual(true); setTagInput(""); return; }
|
|
1809
|
-
apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(id)}`)
|
|
1810
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
1811
|
-
.then((j) => { if (j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
|
|
1812
|
-
.catch(() => {});
|
|
1813
|
-
};
|
|
1938
|
+
// The two-step delete arms per selection: switching or clearing the
|
|
1939
|
+
// target disarms it, so a stale confirm can never delete a new pick.
|
|
1814
1940
|
useEffect(() => {
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(selectedId)}`)
|
|
1818
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
1819
|
-
.then((j) => { if (!cancelled && j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
|
|
1820
|
-
.catch(() => {});
|
|
1821
|
-
} else {
|
|
1822
|
-
setTagTags([]);
|
|
1823
|
-
setTagManual(true);
|
|
1824
|
-
}
|
|
1825
|
-
setTagAdding(false);
|
|
1826
|
-
return () => { cancelled = true; };
|
|
1941
|
+
setConfirmDelete(false);
|
|
1942
|
+
setDeleteError(false);
|
|
1827
1943
|
}, [selectedId]);
|
|
1828
1944
|
|
|
1829
1945
|
const q = query.trim().toLowerCase();
|
|
@@ -1833,13 +1949,12 @@ window.__ModuleLoader__.load({
|
|
|
1833
1949
|
const entityQuery = query.trim().startsWith("entity:")
|
|
1834
1950
|
? query.trim().slice(7).trim()
|
|
1835
1951
|
: "";
|
|
1952
|
+
// Search results were filtered server-side (global); the type rail
|
|
1953
|
+
// still narrows them. Browse mode serves the loaded pages as-is —
|
|
1954
|
+
// type filtering happened in the query.
|
|
1836
1955
|
const visible = remoteItems
|
|
1837
|
-
? remoteItems
|
|
1838
|
-
: items
|
|
1839
|
-
if (type !== "all" && m.type !== type) return false;
|
|
1840
|
-
if (!q) return true;
|
|
1841
|
-
return (m.title || "").toLowerCase().includes(q) || (m.content || "").toLowerCase().includes(q);
|
|
1842
|
-
});
|
|
1956
|
+
? remoteItems.filter((m) => type === "all" || m.type === type)
|
|
1957
|
+
: items;
|
|
1843
1958
|
|
|
1844
1959
|
const counts = {};
|
|
1845
1960
|
for (const m of items) counts[m.type] = (counts[m.type] || 0) + 1;
|
|
@@ -1874,8 +1989,9 @@ window.__ModuleLoader__.load({
|
|
|
1874
1989
|
}
|
|
1875
1990
|
curDay.items.push(m);
|
|
1876
1991
|
}
|
|
1992
|
+
for (const mo of months) mo.count = mo.days.reduce((s, d) => s + d.items.length, 0);
|
|
1877
1993
|
|
|
1878
|
-
const selected =
|
|
1994
|
+
const selected = visible.find((m) => m.id === selectedId) || null;
|
|
1879
1995
|
|
|
1880
1996
|
const copyContent = () => {
|
|
1881
1997
|
if (!selected) return;
|
|
@@ -1885,80 +2001,61 @@ window.__ModuleLoader__.load({
|
|
|
1885
2001
|
);
|
|
1886
2002
|
};
|
|
1887
2003
|
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
1890
|
-
//
|
|
1891
|
-
//
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
if (!
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
}).then((res) => {
|
|
1900
|
-
if (res.status === 409) { setTagManual(false); setTagAdding(false); setTagInput(""); return; }
|
|
1901
|
-
return res.json();
|
|
1902
|
-
}).then((j) => {
|
|
1903
|
-
if (j?.ok) { setTagAdding(false); setTagInput(""); loadTags(selectedId); }
|
|
1904
|
-
}).catch(() => {});
|
|
1905
|
-
};
|
|
1906
|
-
const removeTag = (tag) => {
|
|
1907
|
-
if (!selectedId) return;
|
|
1908
|
-
apiFetch("/api/dsh-mneme/memory/tags", {
|
|
2004
|
+
// Two-step delete (no window.confirm): the red button arms, a second
|
|
2005
|
+
// click commits POST /api/dsh-mneme/delete. On success the row leaves
|
|
2006
|
+
// every local view (browse pages + search results), the selection
|
|
2007
|
+
// clears and the total shrinks; failures — 404 included — surface as
|
|
2008
|
+
// a transient red note next to the actions.
|
|
2009
|
+
const deleteSelected = () => {
|
|
2010
|
+
if (!selected || deleting) return;
|
|
2011
|
+
const id = selected.id;
|
|
2012
|
+
setDeleting(true);
|
|
2013
|
+
setDeleteError(false);
|
|
2014
|
+
apiFetch("/api/dsh-mneme/delete", {
|
|
1909
2015
|
method: "POST",
|
|
1910
2016
|
headers: { "Content-Type": "application/json" },
|
|
1911
|
-
body: JSON.stringify({ id
|
|
1912
|
-
})
|
|
1913
|
-
if (res.
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2017
|
+
body: JSON.stringify({ id })
|
|
2018
|
+
})
|
|
2019
|
+
.then((res) => { if (!res.ok) throw new Error("http"); })
|
|
2020
|
+
.then(() => {
|
|
2021
|
+
setItems((cur) => cur.filter((m) => m.id !== id));
|
|
2022
|
+
setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== id) : cur));
|
|
2023
|
+
setTotal((n) => Math.max(0, n - 1));
|
|
2024
|
+
setSelectedId(null);
|
|
2025
|
+
setConfirmDelete(false);
|
|
2026
|
+
setDeleteMsg(true);
|
|
2027
|
+
setTimeout(() => setDeleteMsg(false), 2500);
|
|
2028
|
+
})
|
|
2029
|
+
.catch(() => {
|
|
2030
|
+
setDeleteError(true);
|
|
2031
|
+
setTimeout(() => setDeleteError(false), 4000);
|
|
2032
|
+
})
|
|
2033
|
+
.finally(() => setDeleting(false));
|
|
1922
2034
|
};
|
|
1923
2035
|
|
|
1924
2036
|
// Graph → memory jump: land on the browser tab with filters reset so
|
|
1925
|
-
// the target row is visible, selected and scrolled into view.
|
|
2037
|
+
// the target row is visible, selected and scrolled into view. A target
|
|
2038
|
+
// beyond the loaded pages renders as a single-row result set instead
|
|
2039
|
+
// of pulling page after page to find it.
|
|
1926
2040
|
const jumpToMemory = (target) => {
|
|
1927
2041
|
if (!target?.id) return;
|
|
1928
2042
|
setView("memory");
|
|
1929
2043
|
setType("all");
|
|
1930
2044
|
setQuery("");
|
|
1931
|
-
|
|
2045
|
+
setRemoteItems(items.some((m) => m.id === target.id) ? null : [target]);
|
|
2046
|
+
setExpandedMonths(null);
|
|
1932
2047
|
setSelectedId(target.id);
|
|
1933
2048
|
};
|
|
1934
2049
|
|
|
1935
|
-
// [[wiki-link]] click in the detail content: resolve the title to a
|
|
1936
|
-
// memory (case-insensitive exact match) then jump to it. Unresolvable
|
|
1937
|
-
// titles silently no-op — the link stays as display text.
|
|
1938
|
-
const onWikilinkClick = (target) => {
|
|
1939
|
-
if (!target) return;
|
|
1940
|
-
apiFetch(`/api/dsh-mneme/wikilinks/resolve?title=${encodeURIComponent(target)}`)
|
|
1941
|
-
.then((r) => (r.ok ? r.json() : null))
|
|
1942
|
-
.then((j) => { if (j?.memory?.id) jumpToMemory(j.memory); })
|
|
1943
|
-
.catch(() => {});
|
|
1944
|
-
};
|
|
1945
|
-
|
|
1946
2050
|
const openGraphFor = (name) => {
|
|
1947
2051
|
setGraphFocus(name || "");
|
|
1948
|
-
setView("
|
|
1949
|
-
};
|
|
1950
|
-
|
|
1951
|
-
// Overview tile click: dive into the memory view filtered to that layer.
|
|
1952
|
-
const pickLayerType = (key) => {
|
|
1953
|
-
setType(key);
|
|
1954
|
-
setView("memory");
|
|
2052
|
+
setView("entity");
|
|
1955
2053
|
};
|
|
1956
2054
|
|
|
1957
2055
|
const subviews = [
|
|
1958
2056
|
{ key: "memory", label: t("memory.explorer.tabMemory") },
|
|
1959
|
-
{ key: "
|
|
1960
|
-
{ key: "
|
|
1961
|
-
{ key: "graph", label: t("memory.explorer.tabGraph") },
|
|
2057
|
+
{ key: "entity", label: t("memory.explorer.tabEntities") },
|
|
2058
|
+
{ key: "status", label: t("memory.explorer.tabStatus") },
|
|
1962
2059
|
{ key: "settings", label: t("memory.explorer.tabSettings") }
|
|
1963
2060
|
];
|
|
1964
2061
|
|
|
@@ -1973,26 +2070,66 @@ window.__ModuleLoader__.load({
|
|
|
1973
2070
|
"aria-pressed": String(view === s.key),
|
|
1974
2071
|
onClick: () => setView(s.key)
|
|
1975
2072
|
},
|
|
1976
|
-
s.key === "
|
|
1977
|
-
|
|
1978
|
-
: s.label
|
|
2073
|
+
h(Icon, { name: s.key === "memory" ? "database" : s.key === "entity" ? "waypoints" : s.key === "status" ? "activity" : "settings", size: 14, className: "mneme-vtabico" }),
|
|
2074
|
+
s.label
|
|
1979
2075
|
))
|
|
1980
2076
|
),
|
|
1981
2077
|
),
|
|
1982
2078
|
view === "memory" && h("div", { className: "mneme-xmain" },
|
|
1983
|
-
|
|
1984
|
-
|
|
2079
|
+
h("div", { className: "mneme-xside" },
|
|
2080
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
|
|
2081
|
+
h("div", { className: "mneme-xsearchwrap" },
|
|
2082
|
+
h(Icon, { name: "search", size: 14, className: "mneme-xsearchico" }),
|
|
2083
|
+
h("input", {
|
|
2084
|
+
className: "mneme-search mneme-xsearch",
|
|
2085
|
+
placeholder: t("memory.explorer.search"),
|
|
2086
|
+
value: query,
|
|
2087
|
+
onChange: (e) => setQuery(e.target.value)
|
|
2088
|
+
})
|
|
2089
|
+
),
|
|
2090
|
+
entityQuery && h("button", {
|
|
2091
|
+
className: "mneme-entitychip",
|
|
2092
|
+
style: { textAlign: "left", justifyContent: "flex-start" },
|
|
2093
|
+
onClick: () => openGraphFor(entityQuery)
|
|
2094
|
+
}, `${t("memory.graph.viewInGraph")} “${entityQuery}”`),
|
|
2095
|
+
h("div", { className: "mneme-xrow" },
|
|
2096
|
+
vecEnabled && h("button", {
|
|
2097
|
+
className: semantic ? "mneme-chip mneme-active" : "mneme-chip",
|
|
2098
|
+
title: t("memory.settings.vectorTitle"),
|
|
2099
|
+
onClick: () => setSemantic(!semantic)
|
|
2100
|
+
}, t("memory.panel.semantic")),
|
|
2101
|
+
h("select", {
|
|
2102
|
+
className: "mneme-select mneme-xselect",
|
|
2103
|
+
value: searchTopK,
|
|
2104
|
+
onChange: (e) => setSearchTopK(Number(e.target.value)),
|
|
2105
|
+
title: t("memory.explorer.topK")
|
|
2106
|
+
}, [5, 10, 20, 50].map((n) => h("option", { key: n, value: n }, t("memory.explorer.topKOption").replace("{n}", String(n)))))
|
|
2107
|
+
),
|
|
2108
|
+
h("div", { className: "mneme-xrow" },
|
|
2109
|
+
h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
|
|
2110
|
+
h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) },
|
|
2111
|
+
h(Icon, { name: "refresh", size: 12 }), t("memory.explorer.refresh"))
|
|
2112
|
+
)
|
|
2113
|
+
),
|
|
2114
|
+
h("div", { className: "mneme-xside mneme-xside--filter" },
|
|
2115
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.importance")),
|
|
2116
|
+
h("div", { className: "mneme-xrow" },
|
|
2117
|
+
[0, 3, 4, 5].map((v) =>
|
|
2118
|
+
h("button", {
|
|
2119
|
+
key: v,
|
|
2120
|
+
className: minImp === v ? "mneme-chip mneme-active" : "mneme-chip",
|
|
2121
|
+
onClick: () => setMinImp(v)
|
|
2122
|
+
}, v === 0 ? t("memory.tab.all") : `★${v}+`))
|
|
2123
|
+
),
|
|
1985
2124
|
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
|
|
1986
2125
|
h("button", {
|
|
1987
2126
|
className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
|
|
1988
|
-
"aria-pressed": type === "all",
|
|
1989
2127
|
onClick: () => setType("all")
|
|
1990
2128
|
}, h("span", { className: "mneme-xdot mneme-xdot--all", "aria-hidden": "true" }), h("span", null, t("memory.tab.all")), h("span", { className: "mneme-xcount2" }, String(items.length))),
|
|
1991
2129
|
knownTypes.concat(extraTypes).map((key) =>
|
|
1992
2130
|
h("button", {
|
|
1993
2131
|
key,
|
|
1994
2132
|
className: type === key ? "mneme-xtype mneme-active" : "mneme-xtype",
|
|
1995
|
-
"aria-pressed": type === key,
|
|
1996
2133
|
onClick: () => setType(key)
|
|
1997
2134
|
},
|
|
1998
2135
|
h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(key) }, title: typeLabel(t, key), "aria-hidden": "true" }),
|
|
@@ -2000,66 +2137,41 @@ window.__ModuleLoader__.load({
|
|
|
2000
2137
|
h("span", { className: "mneme-xcount2" }, String(counts[key]))
|
|
2001
2138
|
))
|
|
2002
2139
|
),
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
// 左卡:搜索
|
|
2006
|
-
h("div", { className: "mneme-card mneme-card--search", role: "region", "aria-label": t("memory.explorer.searchTitle") },
|
|
2007
|
-
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
|
|
2008
|
-
h("input", {
|
|
2009
|
-
className: "mneme-search mneme-xsearch",
|
|
2010
|
-
"aria-label": t("memory.explorer.search"),
|
|
2011
|
-
placeholder: t("memory.explorer.search"),
|
|
2012
|
-
value: query,
|
|
2013
|
-
onChange: (e) => setQuery(e.target.value)
|
|
2014
|
-
}),
|
|
2015
|
-
entityQuery && h("button", {
|
|
2016
|
-
className: "mneme-entitychip",
|
|
2017
|
-
style: { textAlign: "left", justifyContent: "flex-start" },
|
|
2018
|
-
onClick: () => openGraphFor(entityQuery)
|
|
2019
|
-
}, `${t("memory.graph.viewInGraph")} “${entityQuery}”`),
|
|
2020
|
-
h("div", { className: "mneme-xrow" },
|
|
2021
|
-
vecEnabled && h("button", {
|
|
2022
|
-
className: semantic ? "mneme-chip mneme-active" : "mneme-chip",
|
|
2023
|
-
title: t("memory.settings.vectorTitle"),
|
|
2024
|
-
onClick: () => setSemantic(!semantic)
|
|
2025
|
-
}, t("memory.panel.semantic")),
|
|
2026
|
-
h("select", {
|
|
2027
|
-
className: "mneme-select mneme-xselect",
|
|
2028
|
-
value: searchTopK,
|
|
2029
|
-
onChange: (e) => setSearchTopK(Number(e.target.value)),
|
|
2030
|
-
title: t("memory.explorer.topK")
|
|
2031
|
-
}, [5, 10, 20, 50].map((n) => h("option", { key: n, value: n }, t("memory.explorer.topKOption").replace("{n}", String(n)))))
|
|
2032
|
-
),
|
|
2033
|
-
h("div", { className: "mneme-xrow" },
|
|
2034
|
-
h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
|
|
2035
|
-
h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) }, t("memory.explorer.refresh"))
|
|
2036
|
-
)
|
|
2037
|
-
),
|
|
2038
|
-
// 中卡:时间树
|
|
2039
|
-
h("div", { className: "mneme-card mneme-card--tree", role: "region", "aria-label": t("memory.explorer.timeline") },
|
|
2140
|
+
h("div", { className: "mneme-xbrowse" },
|
|
2141
|
+
h("div", { className: "mneme-xtree" },
|
|
2040
2142
|
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.timeline")),
|
|
2041
2143
|
loading
|
|
2042
2144
|
? h("div", { className: "mneme-xempty" }, "…")
|
|
2043
|
-
:
|
|
2044
|
-
? h("div", { className: "mneme-xempty" },
|
|
2045
|
-
|
|
2046
|
-
|
|
2145
|
+
: visible.length === 0
|
|
2146
|
+
? h("div", { className: "mneme-xempty" },
|
|
2147
|
+
h(Icon, { name: "inbox", size: 20, className: "mneme-xemptyico" }),
|
|
2148
|
+
t("memory.explorer.empty"))
|
|
2149
|
+
: months.map((month, mi) => {
|
|
2150
|
+
// Only the newest month starts expanded; older months
|
|
2151
|
+
// stay a one-line header until clicked — a several-
|
|
2152
|
+
// thousand-row store renders a handful of DOM nodes.
|
|
2153
|
+
const open = expandedMonths ? !!expandedMonths[month.key] : mi === 0;
|
|
2154
|
+
return h("div", { key: month.key },
|
|
2047
2155
|
h("button", {
|
|
2048
2156
|
className: "mneme-xmonth",
|
|
2049
|
-
"aria-expanded": String(
|
|
2050
|
-
onClick: () =>
|
|
2157
|
+
"aria-expanded": String(open),
|
|
2158
|
+
onClick: () => setExpandedMonths((c) => {
|
|
2159
|
+
const base = c || (months.length ? { [months[0].key]: true } : {});
|
|
2160
|
+
return { ...base, [month.key]: !open };
|
|
2161
|
+
})
|
|
2051
2162
|
},
|
|
2052
|
-
h("span", { className: "mneme-xcaret" },
|
|
2053
|
-
month.label
|
|
2163
|
+
h("span", { className: "mneme-xcaret" }, h(Icon, { name: open ? "chevronDown" : "chevronRight", size: 12 })),
|
|
2164
|
+
h("span", null, month.label),
|
|
2165
|
+
h("span", { className: "mneme-xmonthcount" }, String(month.count))
|
|
2054
2166
|
),
|
|
2055
|
-
|
|
2167
|
+
open && month.days.map((day) =>
|
|
2056
2168
|
h("div", { key: day.key },
|
|
2057
2169
|
h("button", {
|
|
2058
2170
|
className: "mneme-xday",
|
|
2059
2171
|
"aria-expanded": String(!collapsed[day.key]),
|
|
2060
2172
|
onClick: () => setCollapsed((c) => ({ ...c, [day.key]: !c[day.key] }))
|
|
2061
2173
|
},
|
|
2062
|
-
h("span", { className: "mneme-xcaret" }, collapsed[day.key] ? "
|
|
2174
|
+
h("span", { className: "mneme-xcaret" }, h(Icon, { name: collapsed[day.key] ? "chevronRight" : "chevronDown", size: 11 })),
|
|
2063
2175
|
day.label
|
|
2064
2176
|
),
|
|
2065
2177
|
!collapsed[day.key] && day.items.map((m) => {
|
|
@@ -2078,12 +2190,19 @@ window.__ModuleLoader__.load({
|
|
|
2078
2190
|
h("span", { className: "mneme-xname" }, m.title || m.content?.slice(0, 40))
|
|
2079
2191
|
);
|
|
2080
2192
|
})
|
|
2081
|
-
)
|
|
2082
|
-
)
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2193
|
+
))
|
|
2194
|
+
);
|
|
2195
|
+
}),
|
|
2196
|
+
h("div", { ref: moreRef, className: "mneme-xmore" },
|
|
2197
|
+
canLoadMore
|
|
2198
|
+
? h("button", { className: "mneme-footbtn", disabled: loadingMore, onClick: loadMore },
|
|
2199
|
+
loadingMore ? "…" : `${t("memory.explorer.loadMore")}(${items.length}/${total})`)
|
|
2200
|
+
: (!loading && visible.length > 0 && !q && !remoteItems
|
|
2201
|
+
? h("span", { className: "mneme-xcount" }, `${items.length} / ${total}`)
|
|
2202
|
+
: null)
|
|
2203
|
+
)
|
|
2204
|
+
),
|
|
2205
|
+
h("div", { className: "mneme-xdetail" },
|
|
2087
2206
|
selected
|
|
2088
2207
|
? h(react.Fragment, { key: selected.id },
|
|
2089
2208
|
h("div", { className: "mneme-xdinner" },
|
|
@@ -2099,64 +2218,46 @@ window.__ModuleLoader__.load({
|
|
|
2099
2218
|
h("span", { title: formatDate(selected.created_at) }, `${t("memory.explorer.created")}: ${formatDateShort(selected.created_at)}`),
|
|
2100
2219
|
h("span", { title: formatDate(selected.updated_at) }, `${t("memory.explorer.updated")}: ${formatRelativeTime(selected.updated_at, t)}`)
|
|
2101
2220
|
),
|
|
2102
|
-
h("div", { className: "mneme-xdmeta" },
|
|
2103
|
-
h("span", null, t("memory.explorer.tags"))
|
|
2104
|
-
h("div", { className: "mneme-tagrow" },
|
|
2105
|
-
tagTags.length > 0
|
|
2106
|
-
? tagTags.map((tag) =>
|
|
2107
|
-
h("span", {
|
|
2108
|
-
key: tag,
|
|
2109
|
-
className: "mneme-tagchip",
|
|
2110
|
-
title: `tag:${tag}`,
|
|
2111
|
-
onClick: () => onTagClick(tag)
|
|
2112
|
-
},
|
|
2113
|
-
tag,
|
|
2114
|
-
tagManual && h("button", {
|
|
2115
|
-
type: "button",
|
|
2116
|
-
className: "mneme-tagremove",
|
|
2117
|
-
"aria-label": `${t("memory.explorer.tagRemove")}: ${tag}`,
|
|
2118
|
-
onClick: (e) => { e.stopPropagation(); removeTag(tag); }
|
|
2119
|
-
}, "×")
|
|
2120
|
-
)
|
|
2121
|
-
)
|
|
2122
|
-
: h("span", { className: "mneme-tagempty" }, t("memory.explorer.tagsEmpty")),
|
|
2123
|
-
tagManual && (tagAdding
|
|
2124
|
-
? h("input", {
|
|
2125
|
-
key: selectedId,
|
|
2126
|
-
className: "mneme-taginput",
|
|
2127
|
-
autoFocus: true,
|
|
2128
|
-
value: tagInput,
|
|
2129
|
-
placeholder: t("memory.explorer.tagPlaceholder"),
|
|
2130
|
-
onChange: (e) => setTagInput(e.target.value),
|
|
2131
|
-
onKeyDown: (e) => { if (e.key === "Enter") submitTag(); },
|
|
2132
|
-
onBlur: () => { setTagAdding(false); setTagInput(""); }
|
|
2133
|
-
})
|
|
2134
|
-
: h("button", {
|
|
2135
|
-
type: "button",
|
|
2136
|
-
className: "mneme-tagadd",
|
|
2137
|
-
onClick: () => setTagAdding(true)
|
|
2138
|
-
}, `+ ${t("memory.explorer.tagAdd")}`))
|
|
2139
|
-
)
|
|
2140
|
-
),
|
|
2141
|
-
h("div", { className: "mneme-xdcontent" },
|
|
2142
|
-
selected.content ? wikilinkSegments(selected.content, onWikilinkClick) : selected.content
|
|
2221
|
+
Array.isArray(selected.tags) && selected.tags.length > 0 && h("div", { className: "mneme-xdmeta" },
|
|
2222
|
+
h("span", null, `${t("memory.explorer.tags")}: ${selected.tags.join(" · ")}`)
|
|
2143
2223
|
),
|
|
2224
|
+
h("div", { className: "mneme-xdcontent" }, selected.content),
|
|
2144
2225
|
h("div", { className: "mneme-xdactions" },
|
|
2145
2226
|
h("button", { className: "mneme-footbtn", onClick: copyContent },
|
|
2146
|
-
copied ?
|
|
2147
|
-
|
|
2148
|
-
|
|
2227
|
+
h(Icon, { name: copied ? "check" : "copy", size: 12 }),
|
|
2228
|
+
copied ? t("memory.explorer.copied") : t("memory.explorer.copy")),
|
|
2229
|
+
// two-step delete: red outline arms, solid red commits
|
|
2230
|
+
confirmDelete
|
|
2231
|
+
? h(react.Fragment, null,
|
|
2232
|
+
h("button", {
|
|
2233
|
+
className: "mneme-btn mneme-btndangerconfirm",
|
|
2234
|
+
disabled: deleting,
|
|
2235
|
+
onClick: deleteSelected
|
|
2236
|
+
}, t("memory.explorer.confirmDelete")),
|
|
2237
|
+
h("button", {
|
|
2238
|
+
className: "mneme-btn",
|
|
2239
|
+
onClick: () => setConfirmDelete(false)
|
|
2240
|
+
}, t("memory.explorer.cancel"))
|
|
2241
|
+
)
|
|
2242
|
+
: h("button", {
|
|
2243
|
+
className: "mneme-btn mneme-btndanger",
|
|
2244
|
+
onClick: () => setConfirmDelete(true)
|
|
2245
|
+
}, t("memory.explorer.delete")),
|
|
2246
|
+
deleteError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)" } },
|
|
2247
|
+
t("memory.explorer.deleteFailed"))
|
|
2248
|
+
)
|
|
2149
2249
|
)
|
|
2150
2250
|
)
|
|
2151
|
-
: h("div", { className: "mneme-xempty" },
|
|
2251
|
+
: h("div", { className: "mneme-xempty" },
|
|
2252
|
+
deleteMsg && h("div", { className: "mneme-saved", style: { marginBottom: 6 } }, t("memory.explorer.deleted")),
|
|
2253
|
+
t("memory.explorer.emptyDetail"))
|
|
2152
2254
|
)
|
|
2153
2255
|
)
|
|
2154
2256
|
),
|
|
2155
|
-
view === "
|
|
2156
|
-
view === "
|
|
2157
|
-
view === "
|
|
2158
|
-
|
|
2159
|
-
h("div", { className: "mneme-xsettings-inner" }, h(SettingsContent, { t }))
|
|
2257
|
+
view === "entity" && h(EntityPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
|
|
2258
|
+
view === "status" && h(StatusPanel, { t }),
|
|
2259
|
+
view === "settings" && h("div", { className: "mneme-set" },
|
|
2260
|
+
h("div", { className: "mneme-set-inner" }, h(SettingsContent, { t }))
|
|
2160
2261
|
)
|
|
2161
2262
|
);
|
|
2162
2263
|
}
|
|
@@ -2167,22 +2268,7 @@ window.__ModuleLoader__.load({
|
|
|
2167
2268
|
// opens the full-viewport overlay instead — the library stays reachable
|
|
2168
2269
|
// from every conversation state.
|
|
2169
2270
|
function SidebarTrigger({ wide, t }) {
|
|
2170
|
-
// issue #38: the entrance button is now optional (settings-over-config
|
|
2171
|
-
// toggle, persisted via /api/dsh-mneme/config). While the value loads we
|
|
2172
|
-
// render the button (default = visible) so it never flashes out; if the
|
|
2173
|
-
// user disabled it, it drops away after the config resolves. The library
|
|
2174
|
-
// itself is unaffected — the 记忆库 conversation tab stays.
|
|
2175
|
-
const [visible, setVisible] = useState(true);
|
|
2176
|
-
useEffect(() => {
|
|
2177
|
-
let cancelled = false;
|
|
2178
|
-
apiFetch("/api/dsh-mneme/config")
|
|
2179
|
-
.then((res) => res.json())
|
|
2180
|
-
.then((d) => { if (!cancelled) setVisible(d.config?.showSidebarTrigger !== false); })
|
|
2181
|
-
.catch(() => { if (!cancelled) setVisible(true); });
|
|
2182
|
-
return () => { cancelled = true; };
|
|
2183
|
-
}, []);
|
|
2184
2271
|
const [, setOpen] = useOverlayOpen();
|
|
2185
|
-
if (!visible) return null;
|
|
2186
2272
|
return h("button", {
|
|
2187
2273
|
type: "button",
|
|
2188
2274
|
className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",
|