@modusensus/dsh-mneme 0.7.10 → 0.7.11
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 +368 -0
- package/README.md +298 -169
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/{dsh-mneme/lib → lib}/api.js +50 -295
- package/{dsh-mneme/lib → lib}/client.js +780 -936
- package/{dsh-mneme/src → lib}/config.js +6 -100
- 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 +6 -34
- 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/lib → lib}/settings.js +0 -70
- package/{dsh-mneme/lib → lib}/store.js +35 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +41 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/{dsh-mneme/src → src}/api.js +50 -295
- package/{dsh-mneme/lib → src}/config.js +6 -100
- 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 +6 -34
- 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/src → src}/settings.js +0 -70
- package/{dsh-mneme/src → src}/store.js +35 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +1 -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 +0 -43
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- 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,19 @@ 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.explorer.delete": "删除",
|
|
199
|
+
"memory.explorer.confirmDelete": "确认删除?",
|
|
200
|
+
"memory.explorer.cancel": "取消",
|
|
201
|
+
"memory.explorer.deleted": "已删除",
|
|
202
|
+
"memory.explorer.deleteFailed": "删除失败",
|
|
203
|
+
"memory.status.memories": "记忆总数",
|
|
204
|
+
"memory.status.entities": "实体",
|
|
205
|
+
"memory.status.vector": "向量索引",
|
|
206
|
+
"memory.status.vectorOn": "已启用",
|
|
207
|
+
"memory.status.vectorOff": "未启用",
|
|
208
|
+
"memory.status.llm": "LLM 消耗",
|
|
209
|
+
"memory.status.llmCalls": "近 7 天 · {n} 次调用",
|
|
210
|
+
"memory.status.error": "加载失败"
|
|
185
211
|
},
|
|
186
212
|
en: {
|
|
187
213
|
"memory.panel.empty": "No memories yet",
|
|
@@ -191,8 +217,6 @@ window.__ModuleLoader__.load({
|
|
|
191
217
|
"memory.tab.project": "Projects",
|
|
192
218
|
"memory.tab.decision": "Decisions",
|
|
193
219
|
"memory.tab.history": "History",
|
|
194
|
-
"memory.tab.user": "User",
|
|
195
|
-
"memory.tab.fact": "Facts",
|
|
196
220
|
"memory.settings.title": "Memory Settings",
|
|
197
221
|
"memory.settings.profile": "User Profile",
|
|
198
222
|
"memory.settings.profileHint": "Describe yourself once — the agent reads it every turn.",
|
|
@@ -200,16 +224,6 @@ window.__ModuleLoader__.load({
|
|
|
200
224
|
"memory.settings.profileSaved": "Profile saved",
|
|
201
225
|
"memory.settings.rules": "Rules",
|
|
202
226
|
"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
227
|
"memory.settings.ruleAdd": "Add Rule",
|
|
214
228
|
"memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
|
|
215
229
|
"memory.settings.commands": "Custom Commands",
|
|
@@ -225,10 +239,23 @@ window.__ModuleLoader__.load({
|
|
|
225
239
|
"memory.view.label": "Memory",
|
|
226
240
|
"memory.overlay.close": "Close",
|
|
227
241
|
"memory.explorer.tabMemory": "Memories",
|
|
228
|
-
"memory.explorer.
|
|
229
|
-
"memory.explorer.tabDirectory": "Directory",
|
|
242
|
+
"memory.explorer.tabEntities": "Entities",
|
|
230
243
|
"memory.explorer.tabSettings": "Settings",
|
|
231
|
-
"memory.explorer.
|
|
244
|
+
"memory.explorer.tabStatus": "Status",
|
|
245
|
+
"memory.entities.count": "{n} entities",
|
|
246
|
+
"memory.entities.pick": "Select an entity to see its attributes and relations",
|
|
247
|
+
"memory.entities.none": "No entities yet — turn on entity extraction and people / projects / concepts will accumulate here",
|
|
248
|
+
"memory.entities.attrs": "Attributes",
|
|
249
|
+
"memory.entities.relations": "Relations",
|
|
250
|
+
"memory.entities.mentions": "{n} mentions",
|
|
251
|
+
"memory.entities.lastSeen": "Last seen",
|
|
252
|
+
"memory.entities.graph": "Relation graph",
|
|
253
|
+
"memory.entity.person": "People",
|
|
254
|
+
"memory.entity.project": "Projects",
|
|
255
|
+
"memory.entity.concept": "Concepts",
|
|
256
|
+
"memory.entity.technology": "Technologies",
|
|
257
|
+
"memory.entity.organization": "Organizations",
|
|
258
|
+
"memory.entity.other": "Others",
|
|
232
259
|
"memory.explorer.search": "Search title or content…",
|
|
233
260
|
"memory.explorer.searchTitle": "Semantic Search",
|
|
234
261
|
"memory.explorer.types": "Types",
|
|
@@ -238,34 +265,16 @@ window.__ModuleLoader__.load({
|
|
|
238
265
|
"memory.explorer.copy": "Copy content",
|
|
239
266
|
"memory.explorer.copied": "Copied",
|
|
240
267
|
"memory.explorer.refresh": "Refresh",
|
|
268
|
+
"memory.explorer.loadMore": "Load more",
|
|
241
269
|
"memory.explorer.count": "{n} items",
|
|
242
270
|
"memory.explorer.empty": "No memories yet",
|
|
243
271
|
"memory.explorer.source": "Source",
|
|
244
272
|
"memory.explorer.created": "Created",
|
|
245
273
|
"memory.explorer.updated": "Updated",
|
|
246
274
|
"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
275
|
"memory.explorer.importance": "Importance",
|
|
252
276
|
"memory.explorer.topK": "Results limit",
|
|
253
277
|
"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
278
|
"memory.card.open": "Open full text in the Memory tab",
|
|
270
279
|
"memory.time.now": "just now",
|
|
271
280
|
"memory.time.seconds": "{n}s ago",
|
|
@@ -284,7 +293,7 @@ window.__ModuleLoader__.load({
|
|
|
284
293
|
"memory.graph.sourceMemory": "View source memory",
|
|
285
294
|
"memory.graph.hint": "Drag nodes to rearrange · drag the background to pan · scroll to zoom",
|
|
286
295
|
"memory.graph.resetView": "Reset view",
|
|
287
|
-
"memory.graph.viewInGraph": "
|
|
296
|
+
"memory.graph.viewInGraph": "Open in entity explorer",
|
|
288
297
|
"memory.graph.loading": "Loading…",
|
|
289
298
|
"memory.graph.distance": "{n} hop(s) from root",
|
|
290
299
|
"memory.settings.vectorTitle": "Vector Search",
|
|
@@ -303,11 +312,19 @@ window.__ModuleLoader__.load({
|
|
|
303
312
|
"memory.settings.apiTokenPlaceholder": "Empty = no auth (default)",
|
|
304
313
|
"memory.settings.apiTokenSave": "Save Token",
|
|
305
314
|
"memory.settings.apiTokenSaved": "Token saved",
|
|
306
|
-
"memory.
|
|
307
|
-
"memory.
|
|
308
|
-
"memory.
|
|
309
|
-
"memory.
|
|
310
|
-
"memory.
|
|
315
|
+
"memory.explorer.delete": "Delete",
|
|
316
|
+
"memory.explorer.confirmDelete": "Confirm delete?",
|
|
317
|
+
"memory.explorer.cancel": "Cancel",
|
|
318
|
+
"memory.explorer.deleted": "Deleted",
|
|
319
|
+
"memory.explorer.deleteFailed": "Delete failed",
|
|
320
|
+
"memory.status.memories": "Total memories",
|
|
321
|
+
"memory.status.entities": "Entities",
|
|
322
|
+
"memory.status.vector": "Vector index",
|
|
323
|
+
"memory.status.vectorOn": "Enabled",
|
|
324
|
+
"memory.status.vectorOff": "Disabled",
|
|
325
|
+
"memory.status.llm": "LLM Usage",
|
|
326
|
+
"memory.status.llmCalls": "Last 7 days · {n} calls",
|
|
327
|
+
"memory.status.error": "Failed to load"
|
|
311
328
|
}
|
|
312
329
|
};
|
|
313
330
|
|
|
@@ -375,17 +392,6 @@ window.__ModuleLoader__.load({
|
|
|
375
392
|
".mneme-hint{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
|
|
376
393
|
".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
394
|
".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
395
|
// --- main-area memory library page ---
|
|
390
396
|
".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
397
|
".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 +403,11 @@ window.__ModuleLoader__.load({
|
|
|
397
403
|
".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
404
|
".mneme-xtools{margin-left:auto;display:flex;align-items:center;gap:8px;padding:0 0 0 12px}",
|
|
399
405
|
".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
|
|
400
|
-
// ---
|
|
406
|
+
// --- three-column browse layout: hairline separators, no outer box ---
|
|
407
|
+
".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
|
|
408
|
+
".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}",
|
|
409
|
+
".mneme-xside--filter{width:214px}",
|
|
410
|
+
".mneme-xbrowse{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
|
|
401
411
|
".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
402
412
|
".mneme-xsearch{width:100%}",
|
|
403
413
|
".mneme-xselect{flex:1;min-width:0}",
|
|
@@ -422,35 +432,17 @@ window.__ModuleLoader__.load({
|
|
|
422
432
|
".mneme-xdot{flex:none;display:inline-block;width:8px;height:8px;border-radius:50%;background:currentColor}",
|
|
423
433
|
".mneme-xdot--all{background:transparent;border:1.5px solid currentColor;opacity:.55;box-sizing:border-box}",
|
|
424
434
|
".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}",
|
|
435
|
+
// --- detail column (preview pane sits BELOW the timeline: the list is
|
|
436
|
+
// the primary scan target, the detail grows only when something is
|
|
437
|
+
// selected so the empty state never eats the viewport) ---
|
|
438
|
+
".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)}",
|
|
439
|
+
".mneme-xtree{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
|
|
433
440
|
".mneme-xdinner{max-width:720px}",
|
|
434
441
|
".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
442
|
".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
443
|
".mneme-xdsrc{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}",
|
|
437
444
|
".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
445
|
".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
446
|
// --- graph sub-view (fills the content area under the tabs) ---
|
|
455
447
|
".mneme-graph{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;padding:12px 16px 16px;box-sizing:border-box}",
|
|
456
448
|
".mneme-graphbar{display:flex;gap:8px;align-items:center;flex:none;margin-bottom:10px}",
|
|
@@ -472,38 +464,36 @@ window.__ModuleLoader__.load({
|
|
|
472
464
|
".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
|
|
473
465
|
".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
466
|
".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}",
|
|
467
|
+
// --- entity explorer: rail + layered detail (entities → attrs → relations → graph) ---
|
|
468
|
+
".mneme-ent{flex:1;min-height:0;display:flex;overflow:hidden}",
|
|
469
|
+
".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}",
|
|
470
|
+
".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}",
|
|
471
|
+
".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}",
|
|
472
|
+
".mneme-entitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
473
|
+
".mneme-entitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
|
|
474
|
+
".mneme-entname{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
475
|
+
".mneme-entmentions{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
476
|
+
".mneme-entdetail{flex:1;min-width:0;min-height:0;overflow-y:auto;padding:20px 24px 32px;box-sizing:border-box}",
|
|
477
|
+
".mneme-entinner{max-width:720px}",
|
|
478
|
+
".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}",
|
|
479
|
+
".mneme-enttitle .mneme-xdot{width:10px;height:10px}",
|
|
480
|
+
".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)}",
|
|
481
|
+
".mneme-layer{margin-top:24px}",
|
|
482
|
+
".mneme-layerhead{display:flex;align-items:baseline;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:10px}",
|
|
483
|
+
".mneme-layercount{font-size:12px;font-weight:400;color:var(--dsw-alias-label-tertiary)}",
|
|
484
|
+
".mneme-attrgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}",
|
|
485
|
+
".mneme-attrcard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 12px}",
|
|
486
|
+
".mneme-attrkey{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
487
|
+
".mneme-attrval{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word}",
|
|
488
|
+
".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}",
|
|
489
|
+
".mneme-relrow:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
490
|
+
".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}",
|
|
491
|
+
".mneme-relname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
492
|
+
// --- settings: quiet stacked sections, one concern per section ---
|
|
493
|
+
".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
|
|
494
|
+
".mneme-set-inner{max-width:680px}",
|
|
505
495
|
".mneme-set-sec{padding:20px 0 24px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
506
|
-
".mneme-
|
|
496
|
+
".mneme-set-sec:last-child{border-bottom:none}",
|
|
507
497
|
".mneme-set-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px}",
|
|
508
498
|
".mneme-set-desc{font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);margin-bottom:14px}",
|
|
509
499
|
".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 +504,7 @@ window.__ModuleLoader__.load({
|
|
|
514
504
|
".mneme-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
515
505
|
".mneme-btn:disabled{opacity:.5;cursor:default}",
|
|
516
506
|
".mneme-saved{font-size:12px;color:var(--dsw-alias-state-success,#2a7)}",
|
|
507
|
+
// rule rows: numbered chips + hover-revealed delete
|
|
517
508
|
".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
509
|
".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
510
|
".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 +514,40 @@ window.__ModuleLoader__.load({
|
|
|
523
514
|
".mneme-set-cmd{flex:1;min-width:0}",
|
|
524
515
|
".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--dsw-alias-state-business-primary)}",
|
|
525
516
|
".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}",
|
|
517
|
+
// --- settings sub-view ---
|
|
518
|
+
".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
|
|
553
519
|
// --- hero fallback: full-viewport memory library when no tab ring exists ---
|
|
554
520
|
// The host hides the whole conversation tab ring while a session is
|
|
555
521
|
// blank (hero screen), so the sidebar entry cannot activate the tab
|
|
556
522
|
// there. This surface reuses the exact MemoryExplorer UI at full size —
|
|
557
523
|
// not a side drawer — so the library stays reachable from any state.
|
|
558
524
|
".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)}",
|
|
525
|
+
".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
526
|
".mneme-overlaytitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
565
527
|
".mneme-overlaybody{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
|
|
566
|
-
"@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}"
|
|
528
|
+
"@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}",
|
|
529
|
+
// --- explorer refresh: paged month tree, sticky headers, inline icons ---
|
|
530
|
+
".mneme-vtabico{flex:none;opacity:.85}",
|
|
531
|
+
".mneme-xsearchwrap{position:relative;flex:none}",
|
|
532
|
+
".mneme-xsearchico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--dsw-alias-label-tertiary);pointer-events:none}",
|
|
533
|
+
".mneme-xsearchwrap .mneme-xsearch{padding-left:27px}",
|
|
534
|
+
".mneme-footbtn{display:inline-flex;align-items:center;gap:4px}",
|
|
535
|
+
".mneme-xmonth{position:sticky;top:0;z-index:2;background:var(--dsw-alias-bg-layer-1);display:flex;align-items:center;gap:6px}",
|
|
536
|
+
".mneme-xmonthcount{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-weight:400;font-variant-numeric:tabular-nums}",
|
|
537
|
+
".mneme-xcaret{display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary)}",
|
|
538
|
+
".mneme-xmore{display:flex;justify-content:center;align-items:center;padding:10px 0 24px;min-height:20px}",
|
|
539
|
+
".mneme-xemptyico{display:block;margin:0 auto 6px;opacity:.7}",
|
|
540
|
+
// --- status sub-view: responsive stat-card grid (auto-fill, ~220px min) ---
|
|
541
|
+
".mneme-status{flex:1;min-height:0;overflow-y:auto;padding:16px 20px 32px;box-sizing:border-box}",
|
|
542
|
+
".mneme-statusgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;max-width:960px}",
|
|
543
|
+
".mneme-statuscard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px}",
|
|
544
|
+
".mneme-statusnum{font-size:24px;font-weight:600;line-height:32px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}",
|
|
545
|
+
".mneme-statuscap{margin-top:4px;font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);word-break:break-word}",
|
|
546
|
+
// --- destructive actions: red outline = delete, solid red = confirm ---
|
|
547
|
+
".mneme-btndanger{color:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33)}",
|
|
548
|
+
".mneme-btndanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 12%,transparent)}",
|
|
549
|
+
".mneme-btndangerconfirm{background:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33);color:#fff}",
|
|
550
|
+
".mneme-btndangerconfirm:hover{filter:brightness(.9)}"
|
|
567
551
|
].join("\n");
|
|
568
552
|
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) {
|
|
569
553
|
const tag = document.createElement("style");
|
|
@@ -599,32 +583,28 @@ window.__ModuleLoader__.load({
|
|
|
599
583
|
project: "#22c55e",
|
|
600
584
|
decision: "#3b82f6",
|
|
601
585
|
summary: "#a855f7",
|
|
602
|
-
user: "#f59e0b",
|
|
603
|
-
fact: "#22c55e",
|
|
604
586
|
history: "#94a3b8"
|
|
605
587
|
};
|
|
606
588
|
function memoryTypeColor(type) {
|
|
607
589
|
return MEMORY_TYPE_COLORS[type] || "#94a3b8";
|
|
608
590
|
}
|
|
609
591
|
|
|
610
|
-
// Compact date for the detail meta ("2026/
|
|
592
|
+
// Compact date for the detail meta ("2026/8/16"); the full timestamp
|
|
611
593
|
// stays in the tooltip.
|
|
612
594
|
function formatDateShort(value) {
|
|
613
595
|
if (!value) return "—";
|
|
614
596
|
const date = new Date(value);
|
|
615
597
|
return Number.isNaN(date.getTime()) ? "—" : date.toLocaleDateString();
|
|
616
598
|
}
|
|
599
|
+
// Graph canvas space: the svg's fixed viewBox. The simulation runs in
|
|
600
|
+
// these user units — never in CSS pixels — so node positions always land
|
|
601
|
+
// inside the drawn area; the browser scales the viewBox to whatever the
|
|
602
|
+
// element's CSS size is (small window, hidden tab, maximized #72).
|
|
603
|
+
const VIEW_W = 380, VIEW_H = 300;
|
|
604
|
+
|
|
617
605
|
function nodeRadius(n) {
|
|
618
606
|
// 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));
|
|
607
|
+
return 7 + Math.min(20, Math.max(1, n.mention_count ?? 1)) * 0.55;
|
|
628
608
|
}
|
|
629
609
|
|
|
630
610
|
// Deterministic golden-angle spiral: no two nodes start overlapping, and
|
|
@@ -639,100 +619,17 @@ window.__ModuleLoader__.load({
|
|
|
639
619
|
});
|
|
640
620
|
}
|
|
641
621
|
|
|
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 }) {
|
|
622
|
+
// --- Entity explorer: the entity-gene layers made visible ---
|
|
623
|
+
// Rail = the entity directory (grouped by type, mention-ranked inside a
|
|
624
|
+
// type). Detail = the layered structure from docs/ENTITIES.md:
|
|
625
|
+
// ① 属性 current snapshot of entity_attrs (saveAttr invalidates old rows)
|
|
626
|
+
// ② 关系 entity_relations touching the entity (from the 1-hop ego walk)
|
|
627
|
+
// ③ 关联记忆 memories recalled through the entity: prefix search
|
|
628
|
+
// ④ 关系图谱 the same ego graph the old graph-only tab drew
|
|
629
|
+
function EntityPanel({ t, focusEntity, onJumpMemory }) {
|
|
630
|
+
const [entities, setEntities] = useState(null); // null = still loading
|
|
631
|
+
const [railQuery, setRailQuery] = useState("");
|
|
734
632
|
const [entityName, setEntityName] = useState(focusEntity || "");
|
|
735
|
-
const [inputValue, setInputValue] = useState(focusEntity || "");
|
|
736
633
|
const [depth, setDepth] = useState(1);
|
|
737
634
|
const [data, setData] = useState(null);
|
|
738
635
|
const [status, setStatus] = useState("idle"); // idle | loading | ready | notfound | error
|
|
@@ -745,14 +642,29 @@ window.__ModuleLoader__.load({
|
|
|
745
642
|
const viewRef = useRef({ x: 0, y: 0, k: 1 }); // pan/zoom viewport, viewBox units
|
|
746
643
|
const [frame, setFrame] = useState(0); // re-render tick driven by the simulation
|
|
747
644
|
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
let cancelled = false;
|
|
647
|
+
apiFetch("/api/dsh-mneme/entities?limit=500")
|
|
648
|
+
.then((r) => (r.ok ? r.json() : { entities: [] }))
|
|
649
|
+
.then((j) => { if (!cancelled) setEntities(Array.isArray(j.entities) ? j.entities : []); })
|
|
650
|
+
.catch(() => { if (!cancelled) setEntities([]); });
|
|
651
|
+
return () => { cancelled = true; };
|
|
652
|
+
}, []);
|
|
653
|
+
|
|
748
654
|
useEffect(() => {
|
|
749
655
|
if (focusEntity && focusEntity !== entityName) {
|
|
750
656
|
setEntityName(focusEntity);
|
|
751
|
-
setInputValue(focusEntity);
|
|
752
657
|
}
|
|
753
658
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
754
659
|
}, [focusEntity]);
|
|
755
660
|
|
|
661
|
+
// Auto-select the most recently seen entity so the layers are never a
|
|
662
|
+
// dead end on first open (the rail still allows picking another one).
|
|
663
|
+
useEffect(() => {
|
|
664
|
+
if (!entities || entityName || entities.length === 0) return;
|
|
665
|
+
setEntityName(entities[0].name);
|
|
666
|
+
}, [entities, entityName]);
|
|
667
|
+
|
|
756
668
|
const load = useCallback(async (name, d) => {
|
|
757
669
|
if (!name) { setData(null); setStatus("idle"); return; }
|
|
758
670
|
setStatus("loading");
|
|
@@ -772,31 +684,32 @@ window.__ModuleLoader__.load({
|
|
|
772
684
|
|
|
773
685
|
useEffect(() => { load(entityName, depth); }, [load, entityName, depth]);
|
|
774
686
|
|
|
775
|
-
//
|
|
687
|
+
// Layer data for the selected entity: current attrs + related memories.
|
|
776
688
|
useEffect(() => {
|
|
777
689
|
setAttrs([]);
|
|
778
690
|
setRelated([]);
|
|
779
|
-
if (!
|
|
780
|
-
const name = selected.node.name;
|
|
691
|
+
if (!entityName) return;
|
|
781
692
|
let cancelled = false;
|
|
782
|
-
apiFetch(`/api/dsh-mneme/semantic/graph/entity-attrs?entity=${encodeURIComponent(
|
|
693
|
+
apiFetch(`/api/dsh-mneme/semantic/graph/entity-attrs?entity=${encodeURIComponent(entityName)}`)
|
|
783
694
|
.then((r) => (r.ok ? r.json() : { attrs: [] }))
|
|
784
695
|
.then((j) => { if (!cancelled) setAttrs(Array.isArray(j.attrs) ? j.attrs : []); })
|
|
785
696
|
.catch(() => {});
|
|
786
|
-
apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent("entity:" +
|
|
697
|
+
apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent("entity:" + entityName)}&limit=10`)
|
|
787
698
|
.then((r) => (r.ok ? r.json() : { items: [] }))
|
|
788
699
|
.then((j) => { if (!cancelled) setRelated(Array.isArray(j.items) ? j.items : []); })
|
|
789
700
|
.catch(() => {});
|
|
790
701
|
return () => { cancelled = true; };
|
|
791
|
-
}, [
|
|
702
|
+
}, [entityName]);
|
|
792
703
|
|
|
793
704
|
// Simulation: run in rAF against posRef, tick React only every few frames.
|
|
794
705
|
// Re-seeds when data changes; dragging writes straight into posRef.
|
|
795
706
|
useEffect(() => {
|
|
796
707
|
if (!data || data.nodes.length === 0) return;
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
708
|
+
// Bounds in viewBox units: measuring clientWidth here put the gravity
|
|
709
|
+
// center and clamp range outside the 380-wide viewBox once the window
|
|
710
|
+
// (or its hidden-then-shown container) got wide, and every node was
|
|
711
|
+
// clipped off-canvas — the maximized-invisible bug (#72).
|
|
712
|
+
posRef.current = initialPositions(data.nodes, VIEW_W, VIEW_H);
|
|
800
713
|
const byId = new Map(posRef.current.map((n) => [n.id, n]));
|
|
801
714
|
const edges = data.edges;
|
|
802
715
|
let raf = 0;
|
|
@@ -829,12 +742,12 @@ window.__ModuleLoader__.load({
|
|
|
829
742
|
let energy = 0;
|
|
830
743
|
for (const node of nodes) {
|
|
831
744
|
// gentle gravity toward center keeps the cloud from drifting off-canvas
|
|
832
|
-
node.vx += (
|
|
833
|
-
node.vy += (
|
|
745
|
+
node.vx += (VIEW_W / 2 - node.x) * 0.002;
|
|
746
|
+
node.vy += (VIEW_H / 2 - node.y) * 0.002;
|
|
834
747
|
if (node.pinned || node === dragRef.current?.node) { node.vx = 0; node.vy = 0; continue; }
|
|
835
748
|
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(
|
|
749
|
+
node.x = Math.max(nodeRadius(node) + 4, Math.min(VIEW_W - nodeRadius(node) - 4, node.x + node.vx));
|
|
750
|
+
node.y = Math.max(nodeRadius(node) + 14, Math.min(VIEW_H - nodeRadius(node) - 14, node.y + node.vy));
|
|
838
751
|
energy += Math.abs(node.vx) + Math.abs(node.vy);
|
|
839
752
|
}
|
|
840
753
|
n++;
|
|
@@ -854,7 +767,7 @@ window.__ModuleLoader__.load({
|
|
|
854
767
|
const origX = node.x, origY = node.y;
|
|
855
768
|
const svg = svgRef.current;
|
|
856
769
|
const rect = svg.getBoundingClientRect();
|
|
857
|
-
const scale =
|
|
770
|
+
const scale = VIEW_W / Math.max(1, rect.width) / viewRef.current.k; // viewBox units per css px, zoom-aware
|
|
858
771
|
const onMove = (ev) => {
|
|
859
772
|
dragRef.current.moved = true;
|
|
860
773
|
node.x = origX + (ev.clientX - startX) * scale;
|
|
@@ -870,6 +783,12 @@ window.__ModuleLoader__.load({
|
|
|
870
783
|
window.addEventListener("mouseup", onUp);
|
|
871
784
|
};
|
|
872
785
|
|
|
786
|
+
const onNodeClick = (node) => {
|
|
787
|
+
if (dragRef.current?.moved) return; // it was a drag, not a click
|
|
788
|
+
setSelected({ kind: "node", node });
|
|
789
|
+
};
|
|
790
|
+
const onEdgeClick = (edge) => setSelected({ kind: "edge", edge });
|
|
791
|
+
|
|
873
792
|
// --- canvas pan & zoom ---
|
|
874
793
|
// The viewport (translate + scale) lives in viewRef so the simulation
|
|
875
794
|
// and the handlers share it without re-mounting; every mutation bumps
|
|
@@ -934,39 +853,71 @@ window.__ModuleLoader__.load({
|
|
|
934
853
|
setFrame((f) => f + 1);
|
|
935
854
|
};
|
|
936
855
|
|
|
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
856
|
const nodes = posRef.current;
|
|
944
857
|
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
945
|
-
const VIEW_W = 380, VIEW_H = 300;
|
|
946
858
|
|
|
859
|
+
// --- rail: entity directory grouped by type ---
|
|
860
|
+
const q = railQuery.trim().toLowerCase();
|
|
861
|
+
const visibleEntities = (entities || []).filter((e) => !q || (e.name || "").toLowerCase().includes(q));
|
|
862
|
+
const ENTITY_TYPE_ORDER = ["person", "project", "concept", "technology", "organization"];
|
|
863
|
+
const groups = [];
|
|
864
|
+
const byType = new Map();
|
|
865
|
+
for (const e of visibleEntities) {
|
|
866
|
+
const key = ENTITY_TYPE_ORDER.includes(e.type) ? e.type : "other";
|
|
867
|
+
if (!byType.has(key)) { const g = { key, items: [] }; byType.set(key, g); groups.push(g); }
|
|
868
|
+
byType.get(key).items.push(e);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const entity = entities?.find((e) => e.name === entityName) || null;
|
|
872
|
+
const rootEdges = (data?.edges || []).filter((e) => e.from === data?.root?.id || e.to === data?.root?.id);
|
|
873
|
+
const graphStatus = status === "ready" && data && data.nodes.length <= 1 ? "empty" : status;
|
|
874
|
+
|
|
875
|
+
const rail = h("div", { className: "mneme-entrail" },
|
|
876
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.view.label")),
|
|
877
|
+
h("input", {
|
|
878
|
+
className: "mneme-search mneme-xsearch",
|
|
879
|
+
placeholder: t("memory.graph.placeholder"),
|
|
880
|
+
value: railQuery,
|
|
881
|
+
onChange: (e) => setRailQuery(e.target.value)
|
|
882
|
+
}),
|
|
883
|
+
entities === null && h("div", { className: "mneme-xempty" }, "…"),
|
|
884
|
+
entities !== null && entities.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.entities.none")),
|
|
885
|
+
entities !== null && entities.length > 0 && h("div", { className: "mneme-xcount" },
|
|
886
|
+
t("memory.entities.count").replace("{n}", String(entities.length))),
|
|
887
|
+
groups.map((g) => h("div", { key: g.key },
|
|
888
|
+
h("div", { className: "mneme-enttype" },
|
|
889
|
+
h("span", { className: "mneme-xdot", style: { color: typeColor(g.key === "other" ? null : g.key) }, "aria-hidden": "true" }),
|
|
890
|
+
entityTypeLabel(t, g.key)
|
|
891
|
+
),
|
|
892
|
+
g.items.map((e) => h("button", {
|
|
893
|
+
key: e.id,
|
|
894
|
+
className: e.name === entityName ? "mneme-entitem mneme-active" : "mneme-entitem",
|
|
895
|
+
onClick: () => setEntityName(e.name)
|
|
896
|
+
},
|
|
897
|
+
h("span", { className: "mneme-entname" }, e.name),
|
|
898
|
+
h("span", { className: "mneme-entmentions" }, `×${e.mention_count ?? 1}`)
|
|
899
|
+
))
|
|
900
|
+
))
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// side card for a clicked (non-root) node or edge inside the graph layer
|
|
947
904
|
const side = selected?.kind === "node"
|
|
948
905
|
? h("div", { className: "mneme-graphside" },
|
|
949
906
|
h("div", { className: "mneme-gs-title" },
|
|
950
907
|
h("span", null, selected.node.name),
|
|
951
908
|
h("span", { className: "mneme-gs-meta" },
|
|
952
|
-
`${
|
|
909
|
+
`${entityTypeLabel(t, selected.node.type || "concept")} · ★${selected.node.mention_count ?? 1}`)
|
|
953
910
|
),
|
|
911
|
+
selected.node.name !== entityName && h("button", {
|
|
912
|
+
className: "mneme-footbtn",
|
|
913
|
+
onClick: () => setEntityName(selected.node.name)
|
|
914
|
+
}, t("memory.graph.viewInGraph")),
|
|
954
915
|
attrs.length > 0 && h("div", null,
|
|
955
916
|
h("div", { className: "mneme-gs-meta" }, t("memory.graph.attrs")),
|
|
956
917
|
attrs.map((a, i) => h("div", { key: i, className: "mneme-gs-attr" },
|
|
957
918
|
h("span", { className: "mneme-gs-attrkey" }, `${a.key}:`),
|
|
958
919
|
h("span", { className: "mneme-gs-attrval" }, a.value)
|
|
959
920
|
))
|
|
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
921
|
)
|
|
971
922
|
)
|
|
972
923
|
: selected?.kind === "edge"
|
|
@@ -984,77 +935,140 @@ window.__ModuleLoader__.load({
|
|
|
984
935
|
)
|
|
985
936
|
: null;
|
|
986
937
|
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
},
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
)
|
|
938
|
+
const detail = h("div", { className: "mneme-entdetail" },
|
|
939
|
+
h("div", { className: "mneme-entinner" },
|
|
940
|
+
status === "idle" && h("div", { className: "mneme-hint" }, t("memory.entities.pick")),
|
|
941
|
+
status === "loading" && h("div", { className: "mneme-hint" }, t("memory.graph.loading")),
|
|
942
|
+
status === "notfound" && h("div", { className: "mneme-hint" }, t("memory.graph.notFound")),
|
|
943
|
+
status === "error" && h("div", { className: "mneme-hint" }, t("memory.panel.empty")),
|
|
944
|
+
status === "ready" && entity && h(react.Fragment, null,
|
|
945
|
+
h("div", { className: "mneme-enttitle" },
|
|
946
|
+
h("span", { className: "mneme-xdot", style: { color: typeColor(entity.type) }, "aria-hidden": "true" }),
|
|
947
|
+
entity.name
|
|
948
|
+
),
|
|
949
|
+
h("div", { className: "mneme-entmeta" },
|
|
950
|
+
h("span", null, entityTypeLabel(t, entity.type || "other")),
|
|
951
|
+
h("span", null, t("memory.entities.mentions").replace("{n}", String(entity.mention_count ?? 1))),
|
|
952
|
+
h("span", { title: formatDate(entity.last_seen) },
|
|
953
|
+
`${t("memory.entities.lastSeen")}: ${formatRelativeTime(entity.last_seen, t)}`)
|
|
954
|
+
),
|
|
955
|
+
// ① 属性 — current snapshot (valid_until IS NULL)
|
|
956
|
+
h("div", { className: "mneme-layer" },
|
|
957
|
+
h("div", { className: "mneme-layerhead" },
|
|
958
|
+
t("memory.entities.attrs"),
|
|
959
|
+
h("span", { className: "mneme-layercount" }, String(attrs.length))
|
|
960
|
+
),
|
|
961
|
+
attrs.length === 0
|
|
962
|
+
? h("div", { className: "mneme-xempty" }, t("memory.settings.empty"))
|
|
963
|
+
: h("div", { className: "mneme-attrgrid" },
|
|
964
|
+
attrs.map((a, i) => h("div", { key: i, className: "mneme-attrcard", title: a.value },
|
|
965
|
+
h("div", { className: "mneme-attrkey" }, a.key),
|
|
966
|
+
h("div", { className: "mneme-attrval" }, a.value)
|
|
967
|
+
))
|
|
968
|
+
)
|
|
969
|
+
),
|
|
970
|
+
// ② 关系 — both directions from the 1-hop ego walk
|
|
971
|
+
h("div", { className: "mneme-layer" },
|
|
972
|
+
h("div", { className: "mneme-layerhead" },
|
|
973
|
+
t("memory.entities.relations"),
|
|
974
|
+
h("span", { className: "mneme-layercount" }, String(rootEdges.length))
|
|
975
|
+
),
|
|
976
|
+
rootEdges.length === 0
|
|
977
|
+
? h("div", { className: "mneme-xempty" }, t("memory.settings.empty"))
|
|
978
|
+
: rootEdges.map((e) => {
|
|
979
|
+
const out = e.from === data.root.id;
|
|
980
|
+
const other = nodeById.get(out ? e.to : e.from);
|
|
981
|
+
const otherName = other?.name ?? (out ? e.to : e.from);
|
|
982
|
+
return h("button", {
|
|
983
|
+
key: e.id,
|
|
984
|
+
className: "mneme-relrow",
|
|
985
|
+
title: e.memory_id ? t("memory.graph.sourceMemory") : undefined,
|
|
986
|
+
onClick: () => { if (e.memory_id && onJumpMemory) onJumpMemory({ id: e.memory_id }); }
|
|
987
|
+
},
|
|
988
|
+
h("span", { className: "mneme-relname", style: { textAlign: "right", flex: 1 } }, entity.name),
|
|
989
|
+
h("span", { className: "mneme-reltype" }, e.relation_type),
|
|
990
|
+
h("span", { className: "mneme-relname", style: { flex: 1 } }, otherName)
|
|
991
|
+
);
|
|
992
|
+
})
|
|
993
|
+
),
|
|
994
|
+
// ③ 关联记忆 — entity: prefix recall
|
|
995
|
+
related.length > 0 && h("div", { className: "mneme-layer" },
|
|
996
|
+
h("div", { className: "mneme-layerhead" },
|
|
997
|
+
t("memory.graph.related"),
|
|
998
|
+
h("span", { className: "mneme-layercount" }, String(related.length))
|
|
999
|
+
),
|
|
1000
|
+
related.map((m) => h("button", {
|
|
1001
|
+
key: m.id,
|
|
1002
|
+
type: "button",
|
|
1003
|
+
className: "mneme-gs-link",
|
|
1004
|
+
title: t("memory.card.open"),
|
|
1005
|
+
onClick: () => onJumpMemory && onJumpMemory(m)
|
|
1006
|
+
}, m.title || m.content?.slice(0, 60)))
|
|
1007
|
+
),
|
|
1008
|
+
// ④ 关系图谱 — the ego graph
|
|
1009
|
+
h("div", { className: "mneme-layer" },
|
|
1010
|
+
h("div", { className: "mneme-layerhead" },
|
|
1011
|
+
t("memory.entities.graph"),
|
|
1012
|
+
h("span", { className: "mneme-layercount" },
|
|
1013
|
+
h("button", {
|
|
1014
|
+
className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1015
|
+
title: t("memory.graph.depth"),
|
|
1016
|
+
onClick: () => setDepth(depth === 1 ? 2 : 1)
|
|
1017
|
+
}, `${depth} ${t("memory.graph.depth")}`)),
|
|
1018
|
+
h("span", { className: "mneme-layercount" },
|
|
1019
|
+
h("button", {
|
|
1020
|
+
className: "mneme-chip",
|
|
1021
|
+
title: t("memory.graph.resetView"),
|
|
1022
|
+
onClick: resetView
|
|
1023
|
+
}, t("memory.graph.resetView")))
|
|
1047
1024
|
),
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1025
|
+
graphStatus === "empty"
|
|
1026
|
+
? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
|
|
1027
|
+
: h(react.Fragment, null,
|
|
1028
|
+
h("svg", {
|
|
1029
|
+
ref: svgRef,
|
|
1030
|
+
className: "mneme-graphsvg",
|
|
1031
|
+
viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
|
|
1032
|
+
style: { height: 320 },
|
|
1033
|
+
onMouseDown: onSurfaceMouseDown
|
|
1034
|
+
},
|
|
1035
|
+
h("g", {
|
|
1036
|
+
transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
|
|
1037
|
+
},
|
|
1038
|
+
data.edges.map((e) => {
|
|
1039
|
+
const a = nodeById.get(e.from), b = nodeById.get(e.to);
|
|
1040
|
+
if (!a || !b) return null;
|
|
1041
|
+
return h("line", {
|
|
1042
|
+
key: e.id,
|
|
1043
|
+
x1: a.x, y1: a.y, x2: b.x, y2: b.y,
|
|
1044
|
+
className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
|
|
1045
|
+
onClick: () => onEdgeClick(e)
|
|
1046
|
+
});
|
|
1047
|
+
}),
|
|
1048
|
+
nodes.map((n) => h("g", {
|
|
1049
|
+
key: n.id,
|
|
1050
|
+
className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
|
|
1051
|
+
transform: `translate(${n.x},${n.y})`,
|
|
1052
|
+
onMouseDown: (e) => onNodeMouseDown(e, n),
|
|
1053
|
+
onClick: () => onNodeClick(n),
|
|
1054
|
+
"data-node": n.name
|
|
1055
|
+
},
|
|
1056
|
+
h("circle", { r: nodeRadius(n), fill: typeColor(n.type) }),
|
|
1057
|
+
h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
|
|
1058
|
+
))
|
|
1059
|
+
)
|
|
1060
|
+
),
|
|
1061
|
+
h("div", { className: "mneme-graphhint" }, t("memory.graph.hint"))
|
|
1062
|
+
),
|
|
1063
|
+
side
|
|
1064
|
+
)
|
|
1065
|
+
)
|
|
1066
|
+
)
|
|
1051
1067
|
);
|
|
1068
|
+
|
|
1069
|
+
return h("div", { className: "mneme-ent" }, rail, detail);
|
|
1052
1070
|
}
|
|
1053
1071
|
|
|
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
1072
|
|
|
1059
1073
|
function SettingsContent({ t }) {
|
|
1060
1074
|
const [profile, setProfile] = react.useState("");
|
|
@@ -1072,10 +1086,6 @@ window.__ModuleLoader__.load({
|
|
|
1072
1086
|
(typeof window !== "undefined" && window.localStorage) ? window.localStorage.getItem("dsh-mneme-api-token") || "" : ""
|
|
1073
1087
|
);
|
|
1074
1088
|
const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
|
|
1075
|
-
const [autoTag, setAutoTag] = react.useState(false);
|
|
1076
|
-
const [autoTagSaved, setAutoTagSaved] = react.useState(false);
|
|
1077
|
-
const [showSidebarTrigger, setShowSidebarTrigger] = react.useState(true);
|
|
1078
|
-
const [sidebarTriggerSaved, setSidebarTriggerSaved] = react.useState(false);
|
|
1079
1089
|
|
|
1080
1090
|
const load = react.useCallback(async () => {
|
|
1081
1091
|
try {
|
|
@@ -1089,13 +1099,6 @@ window.__ModuleLoader__.load({
|
|
|
1089
1099
|
setRules(Array.isArray(r.rules) ? r.rules : []);
|
|
1090
1100
|
setCommands(Array.isArray(c.commands) ? c.commands : []);
|
|
1091
1101
|
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
1102
|
} catch { /* ignore */ }
|
|
1100
1103
|
}, []);
|
|
1101
1104
|
|
|
@@ -1192,31 +1195,8 @@ window.__ModuleLoader__.load({
|
|
|
1192
1195
|
} catch { /* ignore */ }
|
|
1193
1196
|
}
|
|
1194
1197
|
|
|
1195
|
-
async function saveAutoTag() {
|
|
1196
|
-
try {
|
|
1197
|
-
await apiFetch("/api/dsh-mneme/config", {
|
|
1198
|
-
method: "PUT",
|
|
1199
|
-
headers: { "Content-Type": "application/json" },
|
|
1200
|
-
body: JSON.stringify({ autoTagEnabled: autoTag })
|
|
1201
|
-
});
|
|
1202
|
-
setAutoTagSaved(true);
|
|
1203
|
-
setTimeout(() => setAutoTagSaved(false), 1500);
|
|
1204
|
-
} catch { /* ignore */ }
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
async function saveSidebarTrigger() {
|
|
1208
|
-
try {
|
|
1209
|
-
await apiFetch("/api/dsh-mneme/config", {
|
|
1210
|
-
method: "PUT",
|
|
1211
|
-
headers: { "Content-Type": "application/json" },
|
|
1212
|
-
body: JSON.stringify({ showSidebarTrigger })
|
|
1213
|
-
});
|
|
1214
|
-
setSidebarTriggerSaved(true);
|
|
1215
|
-
setTimeout(() => setSidebarTriggerSaved(false), 1500);
|
|
1216
|
-
} catch { /* ignore */ }
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
1198
|
return h("div", null,
|
|
1199
|
+
// 用户画像 — who the agent is talking to
|
|
1220
1200
|
h("section", { className: "mneme-set-sec" },
|
|
1221
1201
|
h("div", { className: "mneme-set-title" }, t("memory.settings.profile")),
|
|
1222
1202
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.profileHint")),
|
|
@@ -1231,6 +1211,7 @@ window.__ModuleLoader__.load({
|
|
|
1231
1211
|
saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved"))
|
|
1232
1212
|
)
|
|
1233
1213
|
),
|
|
1214
|
+
// 规则 — numbered rows, hover reveals the delete affordance
|
|
1234
1215
|
h("section", { className: "mneme-set-sec" },
|
|
1235
1216
|
h("div", { className: "mneme-set-title" }, t("memory.settings.rules")),
|
|
1236
1217
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.rulesHint")),
|
|
@@ -1259,6 +1240,7 @@ window.__ModuleLoader__.load({
|
|
|
1259
1240
|
h("button", { className: "mneme-btn", onClick: addRule }, t("memory.settings.ruleAdd"))
|
|
1260
1241
|
)
|
|
1261
1242
|
),
|
|
1243
|
+
// 自定义指令 — slash commands as titled rows
|
|
1262
1244
|
h("section", { className: "mneme-set-sec" },
|
|
1263
1245
|
h("div", { className: "mneme-set-title" }, t("memory.settings.commands")),
|
|
1264
1246
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.commandsHint")),
|
|
@@ -1289,30 +1271,7 @@ window.__ModuleLoader__.load({
|
|
|
1289
1271
|
)
|
|
1290
1272
|
)
|
|
1291
1273
|
),
|
|
1292
|
-
|
|
1293
|
-
h("div", { className: "mneme-set-title" }, t("memory.settings.autoTagTitle")),
|
|
1294
|
-
h("div", { className: "mneme-set-desc" }, t("memory.settings.autoTagHint")),
|
|
1295
|
-
h("label", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 10, fontSize: 13, color: "var(--dsw-alias-label-primary)" } },
|
|
1296
|
-
h("input", { type: "checkbox", checked: autoTag, onChange: (e) => setAutoTag(e.target.checked) }),
|
|
1297
|
-
h("span", null, t("memory.settings.autoTagEnabled"))
|
|
1298
|
-
),
|
|
1299
|
-
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
1300
|
-
h("button", { className: "mneme-btn", onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
|
|
1301
|
-
autoTagSaved && h("span", { className: "mneme-saved" }, t("memory.settings.autoTagSaved"))
|
|
1302
|
-
)
|
|
1303
|
-
),
|
|
1304
|
-
h("section", { className: "mneme-set-sec" },
|
|
1305
|
-
h("div", { className: "mneme-set-title" }, t("memory.settings.sidebarTriggerTitle")),
|
|
1306
|
-
h("div", { className: "mneme-set-desc" }, t("memory.settings.sidebarTriggerHint")),
|
|
1307
|
-
h("label", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 10, fontSize: 13, color: "var(--dsw-alias-label-primary)" } },
|
|
1308
|
-
h("input", { type: "checkbox", checked: showSidebarTrigger, onChange: (e) => setShowSidebarTrigger(e.target.checked) }),
|
|
1309
|
-
h("span", null, t("memory.settings.sidebarTriggerEnabled"))
|
|
1310
|
-
),
|
|
1311
|
-
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
1312
|
-
h("button", { className: "mneme-btn", onClick: saveSidebarTrigger }, t("memory.settings.sidebarTriggerSave")),
|
|
1313
|
-
sidebarTriggerSaved && h("span", { className: "mneme-saved" }, t("memory.settings.sidebarTriggerSaved"))
|
|
1314
|
-
)
|
|
1315
|
-
),
|
|
1274
|
+
// 向量搜索 — semantic recall over an embeddings API
|
|
1316
1275
|
h("section", { className: "mneme-set-sec" },
|
|
1317
1276
|
h("div", { className: "mneme-set-title" }, t("memory.settings.vectorTitle")),
|
|
1318
1277
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.vectorHint")),
|
|
@@ -1330,6 +1289,7 @@ window.__ModuleLoader__.load({
|
|
|
1330
1289
|
reindexMsg && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #666)" } }, reindexMsg)
|
|
1331
1290
|
)
|
|
1332
1291
|
),
|
|
1292
|
+
// API Token — advanced, last
|
|
1333
1293
|
h("section", { className: "mneme-set-sec" },
|
|
1334
1294
|
h("div", { className: "mneme-set-title" }, t("memory.settings.apiTokenTitle")),
|
|
1335
1295
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.apiTokenHint")),
|
|
@@ -1377,9 +1337,6 @@ window.__ModuleLoader__.load({
|
|
|
1377
1337
|
return matches;
|
|
1378
1338
|
}
|
|
1379
1339
|
|
|
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
1340
|
// Click the memory library tab and wait (bounded, rAF-polled) until the
|
|
1384
1341
|
// host marks it selected AND our explorer actually rendered (`.mneme-x`).
|
|
1385
1342
|
// aria-selected alone cannot tell our tab from a same-labelled tab of
|
|
@@ -1454,314 +1411,179 @@ window.__ModuleLoader__.load({
|
|
|
1454
1411
|
return tree;
|
|
1455
1412
|
}
|
|
1456
1413
|
|
|
1457
|
-
|
|
1458
|
-
//
|
|
1459
|
-
|
|
1460
|
-
//
|
|
1461
|
-
//
|
|
1462
|
-
//
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1414
|
+
const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history"];
|
|
1415
|
+
const PAGE_SIZE = 100; // browse page size — the tree grows by 100-row pages
|
|
1416
|
+
|
|
1417
|
+
// --- Status sub-view: a responsive grid of stat cards. Every card owns
|
|
1418
|
+
// its fetch, loading ("…") and error state, so one failing endpoint
|
|
1419
|
+
// never blanks or blocks the others. ---
|
|
1420
|
+
function StatusCard({ t, title, loading, error, num, cap }) {
|
|
1421
|
+
return h("div", { className: "mneme-statuscard" },
|
|
1422
|
+
h("div", { className: "mneme-xcolhead" }, title),
|
|
1423
|
+
loading
|
|
1424
|
+
? h("div", { className: "mneme-statusnum" }, "…")
|
|
1425
|
+
: error
|
|
1426
|
+
? h("div", { className: "mneme-statuscap", style: { color: "var(--dsw-alias-state-error,#c33)" } }, t("memory.status.error"))
|
|
1427
|
+
: h(react.Fragment, null,
|
|
1428
|
+
h("div", { className: "mneme-statusnum" }, num),
|
|
1429
|
+
cap ? h("div", { className: "mneme-statuscap" }, cap) : null
|
|
1430
|
+
)
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1471
1433
|
|
|
1434
|
+
// 记忆总数 — list total plus per-type subtotals; each subtotal request
|
|
1435
|
+
// may fail independently (rendered as "—") without sinking the card.
|
|
1436
|
+
function MemoriesStatusCard({ t }) {
|
|
1437
|
+
const [state, setState] = useState({ loading: true, error: false, total: 0, byType: [] });
|
|
1472
1438
|
useEffect(() => {
|
|
1473
1439
|
let cancelled = false;
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1481
|
-
|
|
1440
|
+
const one = (ty) =>
|
|
1441
|
+
apiFetch(`/api/dsh-mneme/list?limit=1&order=chrono${ty ? `&type=${encodeURIComponent(ty)}` : ""}`)
|
|
1442
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); });
|
|
1443
|
+
one()
|
|
1444
|
+
.then((all) => {
|
|
1445
|
+
if (cancelled) return null;
|
|
1446
|
+
const total = all.total || 0;
|
|
1447
|
+
return Promise.all(EXPLORER_TYPES.map((ty) =>
|
|
1448
|
+
one(ty).then((j) => [ty, j.total || 0]).catch(() => [ty, null])
|
|
1449
|
+
)).then((byType) => {
|
|
1450
|
+
if (!cancelled) setState({ loading: false, error: false, total, byType });
|
|
1451
|
+
});
|
|
1482
1452
|
})
|
|
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(() => {});
|
|
1453
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, byType: [] }); });
|
|
1488
1454
|
return () => { cancelled = true; };
|
|
1489
1455
|
}, []);
|
|
1456
|
+
const cap = state.byType
|
|
1457
|
+
.map(([ty, n]) => `${typeLabel(t, ty)} ${n === null ? "—" : n}`)
|
|
1458
|
+
.join(" · ");
|
|
1459
|
+
return h(StatusCard, {
|
|
1460
|
+
t,
|
|
1461
|
+
title: t("memory.status.memories"),
|
|
1462
|
+
loading: state.loading,
|
|
1463
|
+
error: state.error,
|
|
1464
|
+
num: state.total.toLocaleString(),
|
|
1465
|
+
cap
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1490
1468
|
|
|
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;
|
|
1469
|
+
// 实体 — directory snapshot grouped by entity type.
|
|
1470
|
+
function EntitiesStatusCard({ t }) {
|
|
1471
|
+
const [state, setState] = useState({ loading: true, error: false, total: 0, byType: [] });
|
|
1472
|
+
useEffect(() => {
|
|
1473
|
+
let cancelled = false;
|
|
1474
|
+
const order = ["organization", "person", "project", "technology", "concept"];
|
|
1475
|
+
apiFetch("/api/dsh-mneme/entities?limit=500")
|
|
1476
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
1477
|
+
.then((j) => {
|
|
1478
|
+
if (cancelled) return;
|
|
1479
|
+
const list = Array.isArray(j.entities) ? j.entities : [];
|
|
1480
|
+
const counts = new Map();
|
|
1481
|
+
for (const e of list) {
|
|
1482
|
+
const key = order.includes(e.type) ? e.type : "other";
|
|
1483
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
1532
1484
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
})
|
|
1485
|
+
const byType = order.concat("other")
|
|
1486
|
+
.filter((k) => counts.has(k))
|
|
1487
|
+
.map((k) => [k, counts.get(k)]);
|
|
1488
|
+
setState({ loading: false, error: false, total: list.length, byType });
|
|
1537
1489
|
})
|
|
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
|
-
);
|
|
1490
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, byType: [] }); });
|
|
1491
|
+
return () => { cancelled = true; };
|
|
1492
|
+
}, []);
|
|
1493
|
+
const cap = state.byType.map(([ty, n]) => `${entityTypeLabel(t, ty)} ${n}`).join(" · ");
|
|
1494
|
+
return h(StatusCard, {
|
|
1495
|
+
t,
|
|
1496
|
+
title: t("memory.status.entities"),
|
|
1497
|
+
loading: state.loading,
|
|
1498
|
+
error: state.error,
|
|
1499
|
+
num: state.total.toLocaleString(),
|
|
1500
|
+
cap
|
|
1501
|
+
});
|
|
1605
1502
|
}
|
|
1606
1503
|
|
|
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
|
-
|
|
1504
|
+
// 向量索引 — whether semantic recall is switched on.
|
|
1505
|
+
function VectorStatusCard({ t }) {
|
|
1506
|
+
const [state, setState] = useState({ loading: true, error: false, enabled: false });
|
|
1623
1507
|
useEffect(() => {
|
|
1624
1508
|
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([]); });
|
|
1509
|
+
apiFetch("/api/dsh-mneme/vector-config")
|
|
1510
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
1511
|
+
.then((j) => { if (!cancelled) setState({ loading: false, error: false, enabled: !!j.config?.enabled }); })
|
|
1512
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, enabled: false }); });
|
|
1633
1513
|
return () => { cancelled = true; };
|
|
1634
1514
|
}, []);
|
|
1515
|
+
return h(StatusCard, {
|
|
1516
|
+
t,
|
|
1517
|
+
title: t("memory.status.vector"),
|
|
1518
|
+
loading: state.loading,
|
|
1519
|
+
error: state.error,
|
|
1520
|
+
num: state.enabled ? t("memory.status.vectorOn") : t("memory.status.vectorOff"),
|
|
1521
|
+
cap: ""
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1635
1524
|
|
|
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.
|
|
1525
|
+
// LLM 消耗 — calls + tokens over the trailing 7 days.
|
|
1526
|
+
function LlmStatusCard({ t }) {
|
|
1527
|
+
const [state, setState] = useState({ loading: true, error: false, calls: 0, tokens: 0 });
|
|
1644
1528
|
useEffect(() => {
|
|
1645
|
-
if (!stats || gridTypes.length === 0) return;
|
|
1646
1529
|
let cancelled = false;
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
.
|
|
1652
|
-
|
|
1530
|
+
apiFetch("/api/dsh-mneme/semantic/llm-audit/stats?days=7")
|
|
1531
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
1532
|
+
.then((j) => {
|
|
1533
|
+
if (cancelled) return;
|
|
1534
|
+
const s = j && typeof j === "object" ? (j.stats || j) : {};
|
|
1535
|
+
setState({ loading: false, error: false, calls: Number(s.total_calls ?? 0), tokens: Number(s.total_tokens ?? 0) });
|
|
1536
|
+
})
|
|
1537
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, calls: 0, tokens: 0 }); });
|
|
1653
1538
|
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
|
-
};
|
|
1539
|
+
}, []);
|
|
1540
|
+
return h(StatusCard, {
|
|
1541
|
+
t,
|
|
1542
|
+
title: t("memory.status.llm"),
|
|
1543
|
+
loading: state.loading,
|
|
1544
|
+
error: state.error,
|
|
1545
|
+
num: state.tokens.toLocaleString(),
|
|
1546
|
+
cap: t("memory.status.llmCalls").replace("{n}", state.calls.toLocaleString())
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1708
1549
|
|
|
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))
|
|
1550
|
+
function StatusPanel({ t }) {
|
|
1551
|
+
return h("div", { className: "mneme-status" },
|
|
1552
|
+
h("div", { className: "mneme-statusgrid" },
|
|
1553
|
+
h(MemoriesStatusCard, { t }),
|
|
1554
|
+
h(EntitiesStatusCard, { t }),
|
|
1555
|
+
h(VectorStatusCard, { t }),
|
|
1556
|
+
h(LlmStatusCard, { t })
|
|
1732
1557
|
)
|
|
1733
1558
|
);
|
|
1734
1559
|
}
|
|
1735
1560
|
|
|
1736
|
-
const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history", "user", "fact"];
|
|
1737
1561
|
|
|
1738
1562
|
function MemoryExplorer({ t }) {
|
|
1739
|
-
const [view, setView] = useState("memory"); // memory |
|
|
1740
|
-
const [items, setItems] = useState([]);
|
|
1563
|
+
const [view, setView] = useState("memory"); // memory | entity | status | settings
|
|
1564
|
+
const [items, setItems] = useState([]); // loaded browse pages, chrono desc
|
|
1565
|
+
const [total, setTotal] = useState(0); // server count for the current type filter
|
|
1741
1566
|
const [loading, setLoading] = useState(true);
|
|
1567
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
1742
1568
|
const [type, setType] = useState("all");
|
|
1569
|
+
const [minImp, setMinImp] = useState(0); // importance floor: 0 = all, else 3/4/5
|
|
1743
1570
|
const [query, setQuery] = useState("");
|
|
1744
1571
|
const [semantic, setSemantic] = useState(false);
|
|
1745
1572
|
const [vecEnabled, setVecEnabled] = useState(false);
|
|
1746
1573
|
const [searchTopK, setSearchTopK] = useState(20);
|
|
1747
1574
|
const [remoteItems, setRemoteItems] = useState(null);
|
|
1748
1575
|
const [selectedId, setSelectedId] = useState(null);
|
|
1576
|
+
const [expandedMonths, setExpandedMonths] = useState(null); // null = 仅最新一个月展开
|
|
1749
1577
|
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
1578
|
const [copied, setCopied] = useState(false);
|
|
1755
1579
|
const [reloadKey, setReloadKey] = useState(0);
|
|
1580
|
+
const [confirmDelete, setConfirmDelete] = useState(false); // two-step delete armed
|
|
1581
|
+
const [deleting, setDeleting] = useState(false);
|
|
1582
|
+
const [deleteError, setDeleteError] = useState(false);
|
|
1583
|
+
const [deleteMsg, setDeleteMsg] = useState(false); // transient "deleted" notice
|
|
1756
1584
|
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
1585
|
const itemRefs = useRef(new Map());
|
|
1586
|
+
const moreRef = useRef(null);
|
|
1765
1587
|
|
|
1766
1588
|
useEffect(() => {
|
|
1767
1589
|
apiFetch("/api/dsh-mneme/vector-config")
|
|
@@ -1770,60 +1592,112 @@ window.__ModuleLoader__.load({
|
|
|
1770
1592
|
.catch(() => {});
|
|
1771
1593
|
}, []);
|
|
1772
1594
|
|
|
1595
|
+
// One query string behind every browse-list fetch (initial page,
|
|
1596
|
+
// loadMore, auto-refresh): the type filter and the importance floor
|
|
1597
|
+
// always travel together, so counts and pagination stay consistent.
|
|
1598
|
+
const filterQS = (type === "all" ? "" : `&type=${encodeURIComponent(type)}`)
|
|
1599
|
+
+ (minImp ? `&minImportance=${minImp}` : "");
|
|
1600
|
+
const listUrl = (offset) => `/api/dsh-mneme/list?limit=${PAGE_SIZE}&offset=${offset}${filterQS}&order=chrono`;
|
|
1601
|
+
|
|
1602
|
+
// Browse = paged, pure-chronological pages (order=chrono). The default
|
|
1603
|
+
// importance ordering would interleave months across pages and break
|
|
1604
|
+
// the month tree; type filters server-side so pages stay consistent
|
|
1605
|
+
// however large the store grows.
|
|
1773
1606
|
useEffect(() => {
|
|
1774
1607
|
let cancelled = false;
|
|
1775
1608
|
setLoading(true);
|
|
1776
|
-
apiFetch(
|
|
1777
|
-
.then((res) => (res.ok ? res.json() : { items: [] }))
|
|
1778
|
-
.then((d) => {
|
|
1779
|
-
|
|
1609
|
+
apiFetch(listUrl(0))
|
|
1610
|
+
.then((res) => (res.ok ? res.json() : { items: [], total: 0 }))
|
|
1611
|
+
.then((d) => {
|
|
1612
|
+
if (cancelled) return;
|
|
1613
|
+
setItems(d.items || []);
|
|
1614
|
+
setTotal(d.total || 0);
|
|
1615
|
+
setExpandedMonths(null); // 新数据回到「仅最新月展开」
|
|
1616
|
+
setLoading(false);
|
|
1617
|
+
})
|
|
1618
|
+
.catch(() => { if (!cancelled) { setItems([]); setTotal(0); setLoading(false); } });
|
|
1780
1619
|
return () => { cancelled = true; };
|
|
1781
|
-
}, [reloadKey]);
|
|
1620
|
+
}, [reloadKey, filterQS]);
|
|
1621
|
+
|
|
1622
|
+
const canLoadMore = !query.trim() && !remoteItems && items.length < total;
|
|
1623
|
+
const loadMore = useCallback(() => {
|
|
1624
|
+
if (loadingMore || query.trim() || remoteItems) return;
|
|
1625
|
+
if (items.length >= total) return;
|
|
1626
|
+
setLoadingMore(true);
|
|
1627
|
+
apiFetch(listUrl(items.length))
|
|
1628
|
+
.then((res) => (res.ok ? res.json() : { items: [] }))
|
|
1629
|
+
.then((d) => setItems((cur) => cur.concat(d.items || [])))
|
|
1630
|
+
.catch(() => {})
|
|
1631
|
+
.finally(() => setLoadingMore(false));
|
|
1632
|
+
}, [items.length, total, loadingMore, query, remoteItems, filterQS]);
|
|
1782
1633
|
|
|
1783
|
-
//
|
|
1784
|
-
// while
|
|
1785
|
-
// go server-side (searchMemories resolves them without vector support),
|
|
1786
|
-
// so they bypass the semantic toggle and use the same search endpoint.
|
|
1634
|
+
// Infinite scroll: a sentinel just past the loaded rows pulls the next
|
|
1635
|
+
// page while browsing (search results arrive complete already).
|
|
1787
1636
|
useEffect(() => {
|
|
1788
|
-
const
|
|
1789
|
-
if (
|
|
1637
|
+
const el = moreRef.current;
|
|
1638
|
+
if (!el || !canLoadMore) return undefined;
|
|
1639
|
+
const io = new IntersectionObserver((entries) => {
|
|
1640
|
+
if (entries.some((e) => e.isIntersecting)) loadMore();
|
|
1641
|
+
}, { root: el.closest(".mneme-xtree"), rootMargin: "240px" });
|
|
1642
|
+
io.observe(el);
|
|
1643
|
+
return () => io.disconnect();
|
|
1644
|
+
}, [canLoadMore, loadMore]);
|
|
1645
|
+
|
|
1646
|
+
// Search hits the server so matches are global — not limited to the
|
|
1647
|
+
// loaded pages: keyword = literal text; vector = semantic ranking
|
|
1648
|
+
// (the pipeline falls back to keyword when no embedder is available).
|
|
1649
|
+
useEffect(() => {
|
|
1650
|
+
const query0 = query.trim();
|
|
1651
|
+
if (!query0 || query0.startsWith("entity:")) { setRemoteItems(null); return; }
|
|
1790
1652
|
let cancelled = false;
|
|
1791
1653
|
const timer = setTimeout(() => {
|
|
1792
|
-
|
|
1654
|
+
const mode = semantic ? "vector" : "keyword";
|
|
1655
|
+
apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent(query0)}&mode=${mode}&topK=${searchTopK}`)
|
|
1793
1656
|
.then((res) => (res.ok ? res.json() : { items: [] }))
|
|
1794
1657
|
.then((d) => { if (!cancelled) setRemoteItems(d.items || []); })
|
|
1795
1658
|
.catch(() => { if (!cancelled) setRemoteItems([]); });
|
|
1796
1659
|
}, 250);
|
|
1797
1660
|
return () => { cancelled = true; clearTimeout(timer); };
|
|
1798
|
-
}, [
|
|
1661
|
+
}, [query, semantic, searchTopK]);
|
|
1662
|
+
|
|
1663
|
+
// Live view: while the memory tab is open, quietly re-fetch page 1
|
|
1664
|
+
// every 30s (and on every activation) so fresh memories surface
|
|
1665
|
+
// without a manual refresh. Later pages stay as loaded.
|
|
1666
|
+
const refreshFirstPage = useCallback(() => {
|
|
1667
|
+
if (query.trim() || remoteItems) return;
|
|
1668
|
+
apiFetch(listUrl(0))
|
|
1669
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1670
|
+
.then((d) => {
|
|
1671
|
+
if (!d) return;
|
|
1672
|
+
setTotal(d.total || 0);
|
|
1673
|
+
setItems((cur) => {
|
|
1674
|
+
const fresh = d.items || [];
|
|
1675
|
+
const seen = new Set(fresh.map((m) => m.id));
|
|
1676
|
+
return fresh.concat(cur.filter((m) => !seen.has(m.id)));
|
|
1677
|
+
});
|
|
1678
|
+
})
|
|
1679
|
+
.catch(() => {});
|
|
1680
|
+
}, [query, remoteItems, filterQS]);
|
|
1681
|
+
|
|
1682
|
+
useEffect(() => {
|
|
1683
|
+
if (view !== "memory") return undefined;
|
|
1684
|
+
refreshFirstPage();
|
|
1685
|
+
const iv = setInterval(() => {
|
|
1686
|
+
if (document.visibilityState === "visible") refreshFirstPage();
|
|
1687
|
+
}, 30000);
|
|
1688
|
+
return () => clearInterval(iv);
|
|
1689
|
+
}, [view, refreshFirstPage]);
|
|
1799
1690
|
|
|
1800
1691
|
useEffect(() => {
|
|
1801
1692
|
if (!selectedId) return;
|
|
1802
1693
|
itemRefs.current.get(selectedId)?.scrollIntoView({ block: "nearest" });
|
|
1803
1694
|
}, [selectedId]);
|
|
1804
1695
|
|
|
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
|
-
};
|
|
1696
|
+
// The two-step delete arms per selection: switching or clearing the
|
|
1697
|
+
// target disarms it, so a stale confirm can never delete a new pick.
|
|
1814
1698
|
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; };
|
|
1699
|
+
setConfirmDelete(false);
|
|
1700
|
+
setDeleteError(false);
|
|
1827
1701
|
}, [selectedId]);
|
|
1828
1702
|
|
|
1829
1703
|
const q = query.trim().toLowerCase();
|
|
@@ -1833,13 +1707,12 @@ window.__ModuleLoader__.load({
|
|
|
1833
1707
|
const entityQuery = query.trim().startsWith("entity:")
|
|
1834
1708
|
? query.trim().slice(7).trim()
|
|
1835
1709
|
: "";
|
|
1710
|
+
// Search results were filtered server-side (global); the type rail
|
|
1711
|
+
// still narrows them. Browse mode serves the loaded pages as-is —
|
|
1712
|
+
// type filtering happened in the query.
|
|
1836
1713
|
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
|
-
});
|
|
1714
|
+
? remoteItems.filter((m) => type === "all" || m.type === type)
|
|
1715
|
+
: items;
|
|
1843
1716
|
|
|
1844
1717
|
const counts = {};
|
|
1845
1718
|
for (const m of items) counts[m.type] = (counts[m.type] || 0) + 1;
|
|
@@ -1874,8 +1747,9 @@ window.__ModuleLoader__.load({
|
|
|
1874
1747
|
}
|
|
1875
1748
|
curDay.items.push(m);
|
|
1876
1749
|
}
|
|
1750
|
+
for (const mo of months) mo.count = mo.days.reduce((s, d) => s + d.items.length, 0);
|
|
1877
1751
|
|
|
1878
|
-
const selected =
|
|
1752
|
+
const selected = visible.find((m) => m.id === selectedId) || null;
|
|
1879
1753
|
|
|
1880
1754
|
const copyContent = () => {
|
|
1881
1755
|
if (!selected) return;
|
|
@@ -1885,80 +1759,61 @@ window.__ModuleLoader__.load({
|
|
|
1885
1759
|
);
|
|
1886
1760
|
};
|
|
1887
1761
|
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
1890
|
-
//
|
|
1891
|
-
//
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
if (!
|
|
1895
|
-
|
|
1762
|
+
// Two-step delete (no window.confirm): the red button arms, a second
|
|
1763
|
+
// click commits POST /api/dsh-mneme/delete. On success the row leaves
|
|
1764
|
+
// every local view (browse pages + search results), the selection
|
|
1765
|
+
// clears and the total shrinks; failures — 404 included — surface as
|
|
1766
|
+
// a transient red note next to the actions.
|
|
1767
|
+
const deleteSelected = () => {
|
|
1768
|
+
if (!selected || deleting) return;
|
|
1769
|
+
const id = selected.id;
|
|
1770
|
+
setDeleting(true);
|
|
1771
|
+
setDeleteError(false);
|
|
1772
|
+
apiFetch("/api/dsh-mneme/delete", {
|
|
1896
1773
|
method: "POST",
|
|
1897
1774
|
headers: { "Content-Type": "application/json" },
|
|
1898
|
-
body: JSON.stringify({ id
|
|
1899
|
-
})
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
}).then((j) => {
|
|
1916
|
-
if (j?.ok) loadTags(selectedId);
|
|
1917
|
-
}).catch(() => {});
|
|
1918
|
-
};
|
|
1919
|
-
const onTagClick = (tag) => {
|
|
1920
|
-
setType("all");
|
|
1921
|
-
setQuery(`tag:${tag}`);
|
|
1775
|
+
body: JSON.stringify({ id })
|
|
1776
|
+
})
|
|
1777
|
+
.then((res) => { if (!res.ok) throw new Error("http"); })
|
|
1778
|
+
.then(() => {
|
|
1779
|
+
setItems((cur) => cur.filter((m) => m.id !== id));
|
|
1780
|
+
setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== id) : cur));
|
|
1781
|
+
setTotal((n) => Math.max(0, n - 1));
|
|
1782
|
+
setSelectedId(null);
|
|
1783
|
+
setConfirmDelete(false);
|
|
1784
|
+
setDeleteMsg(true);
|
|
1785
|
+
setTimeout(() => setDeleteMsg(false), 2500);
|
|
1786
|
+
})
|
|
1787
|
+
.catch(() => {
|
|
1788
|
+
setDeleteError(true);
|
|
1789
|
+
setTimeout(() => setDeleteError(false), 4000);
|
|
1790
|
+
})
|
|
1791
|
+
.finally(() => setDeleting(false));
|
|
1922
1792
|
};
|
|
1923
1793
|
|
|
1924
1794
|
// Graph → memory jump: land on the browser tab with filters reset so
|
|
1925
|
-
// the target row is visible, selected and scrolled into view.
|
|
1795
|
+
// the target row is visible, selected and scrolled into view. A target
|
|
1796
|
+
// beyond the loaded pages renders as a single-row result set instead
|
|
1797
|
+
// of pulling page after page to find it.
|
|
1926
1798
|
const jumpToMemory = (target) => {
|
|
1927
1799
|
if (!target?.id) return;
|
|
1928
1800
|
setView("memory");
|
|
1929
1801
|
setType("all");
|
|
1930
1802
|
setQuery("");
|
|
1931
|
-
|
|
1803
|
+
setRemoteItems(items.some((m) => m.id === target.id) ? null : [target]);
|
|
1804
|
+
setExpandedMonths(null);
|
|
1932
1805
|
setSelectedId(target.id);
|
|
1933
1806
|
};
|
|
1934
1807
|
|
|
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
1808
|
const openGraphFor = (name) => {
|
|
1947
1809
|
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");
|
|
1810
|
+
setView("entity");
|
|
1955
1811
|
};
|
|
1956
1812
|
|
|
1957
1813
|
const subviews = [
|
|
1958
1814
|
{ key: "memory", label: t("memory.explorer.tabMemory") },
|
|
1959
|
-
{ key: "
|
|
1960
|
-
{ key: "
|
|
1961
|
-
{ key: "graph", label: t("memory.explorer.tabGraph") },
|
|
1815
|
+
{ key: "entity", label: t("memory.explorer.tabEntities") },
|
|
1816
|
+
{ key: "status", label: t("memory.explorer.tabStatus") },
|
|
1962
1817
|
{ key: "settings", label: t("memory.explorer.tabSettings") }
|
|
1963
1818
|
];
|
|
1964
1819
|
|
|
@@ -1973,26 +1828,66 @@ window.__ModuleLoader__.load({
|
|
|
1973
1828
|
"aria-pressed": String(view === s.key),
|
|
1974
1829
|
onClick: () => setView(s.key)
|
|
1975
1830
|
},
|
|
1976
|
-
s.key === "
|
|
1977
|
-
|
|
1978
|
-
: s.label
|
|
1831
|
+
h(Icon, { name: s.key === "memory" ? "database" : s.key === "entity" ? "waypoints" : s.key === "status" ? "activity" : "settings", size: 14, className: "mneme-vtabico" }),
|
|
1832
|
+
s.label
|
|
1979
1833
|
))
|
|
1980
1834
|
),
|
|
1981
1835
|
),
|
|
1982
1836
|
view === "memory" && h("div", { className: "mneme-xmain" },
|
|
1983
|
-
|
|
1984
|
-
|
|
1837
|
+
h("div", { className: "mneme-xside" },
|
|
1838
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
|
|
1839
|
+
h("div", { className: "mneme-xsearchwrap" },
|
|
1840
|
+
h(Icon, { name: "search", size: 14, className: "mneme-xsearchico" }),
|
|
1841
|
+
h("input", {
|
|
1842
|
+
className: "mneme-search mneme-xsearch",
|
|
1843
|
+
placeholder: t("memory.explorer.search"),
|
|
1844
|
+
value: query,
|
|
1845
|
+
onChange: (e) => setQuery(e.target.value)
|
|
1846
|
+
})
|
|
1847
|
+
),
|
|
1848
|
+
entityQuery && h("button", {
|
|
1849
|
+
className: "mneme-entitychip",
|
|
1850
|
+
style: { textAlign: "left", justifyContent: "flex-start" },
|
|
1851
|
+
onClick: () => openGraphFor(entityQuery)
|
|
1852
|
+
}, `${t("memory.graph.viewInGraph")} “${entityQuery}”`),
|
|
1853
|
+
h("div", { className: "mneme-xrow" },
|
|
1854
|
+
vecEnabled && h("button", {
|
|
1855
|
+
className: semantic ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1856
|
+
title: t("memory.settings.vectorTitle"),
|
|
1857
|
+
onClick: () => setSemantic(!semantic)
|
|
1858
|
+
}, t("memory.panel.semantic")),
|
|
1859
|
+
h("select", {
|
|
1860
|
+
className: "mneme-select mneme-xselect",
|
|
1861
|
+
value: searchTopK,
|
|
1862
|
+
onChange: (e) => setSearchTopK(Number(e.target.value)),
|
|
1863
|
+
title: t("memory.explorer.topK")
|
|
1864
|
+
}, [5, 10, 20, 50].map((n) => h("option", { key: n, value: n }, t("memory.explorer.topKOption").replace("{n}", String(n)))))
|
|
1865
|
+
),
|
|
1866
|
+
h("div", { className: "mneme-xrow" },
|
|
1867
|
+
h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
|
|
1868
|
+
h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) },
|
|
1869
|
+
h(Icon, { name: "refresh", size: 12 }), t("memory.explorer.refresh"))
|
|
1870
|
+
)
|
|
1871
|
+
),
|
|
1872
|
+
h("div", { className: "mneme-xside mneme-xside--filter" },
|
|
1873
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.importance")),
|
|
1874
|
+
h("div", { className: "mneme-xrow" },
|
|
1875
|
+
[0, 3, 4, 5].map((v) =>
|
|
1876
|
+
h("button", {
|
|
1877
|
+
key: v,
|
|
1878
|
+
className: minImp === v ? "mneme-chip mneme-active" : "mneme-chip",
|
|
1879
|
+
onClick: () => setMinImp(v)
|
|
1880
|
+
}, v === 0 ? t("memory.tab.all") : `★${v}+`))
|
|
1881
|
+
),
|
|
1985
1882
|
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
|
|
1986
1883
|
h("button", {
|
|
1987
1884
|
className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
|
|
1988
|
-
"aria-pressed": type === "all",
|
|
1989
1885
|
onClick: () => setType("all")
|
|
1990
1886
|
}, 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
1887
|
knownTypes.concat(extraTypes).map((key) =>
|
|
1992
1888
|
h("button", {
|
|
1993
1889
|
key,
|
|
1994
1890
|
className: type === key ? "mneme-xtype mneme-active" : "mneme-xtype",
|
|
1995
|
-
"aria-pressed": type === key,
|
|
1996
1891
|
onClick: () => setType(key)
|
|
1997
1892
|
},
|
|
1998
1893
|
h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(key) }, title: typeLabel(t, key), "aria-hidden": "true" }),
|
|
@@ -2000,66 +1895,41 @@ window.__ModuleLoader__.load({
|
|
|
2000
1895
|
h("span", { className: "mneme-xcount2" }, String(counts[key]))
|
|
2001
1896
|
))
|
|
2002
1897
|
),
|
|
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") },
|
|
1898
|
+
h("div", { className: "mneme-xbrowse" },
|
|
1899
|
+
h("div", { className: "mneme-xtree" },
|
|
2040
1900
|
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.timeline")),
|
|
2041
1901
|
loading
|
|
2042
1902
|
? h("div", { className: "mneme-xempty" }, "…")
|
|
2043
|
-
:
|
|
2044
|
-
? h("div", { className: "mneme-xempty" },
|
|
2045
|
-
|
|
2046
|
-
|
|
1903
|
+
: visible.length === 0
|
|
1904
|
+
? h("div", { className: "mneme-xempty" },
|
|
1905
|
+
h(Icon, { name: "inbox", size: 20, className: "mneme-xemptyico" }),
|
|
1906
|
+
t("memory.explorer.empty"))
|
|
1907
|
+
: months.map((month, mi) => {
|
|
1908
|
+
// Only the newest month starts expanded; older months
|
|
1909
|
+
// stay a one-line header until clicked — a several-
|
|
1910
|
+
// thousand-row store renders a handful of DOM nodes.
|
|
1911
|
+
const open = expandedMonths ? !!expandedMonths[month.key] : mi === 0;
|
|
1912
|
+
return h("div", { key: month.key },
|
|
2047
1913
|
h("button", {
|
|
2048
1914
|
className: "mneme-xmonth",
|
|
2049
|
-
"aria-expanded": String(
|
|
2050
|
-
onClick: () =>
|
|
1915
|
+
"aria-expanded": String(open),
|
|
1916
|
+
onClick: () => setExpandedMonths((c) => {
|
|
1917
|
+
const base = c || (months.length ? { [months[0].key]: true } : {});
|
|
1918
|
+
return { ...base, [month.key]: !open };
|
|
1919
|
+
})
|
|
2051
1920
|
},
|
|
2052
|
-
h("span", { className: "mneme-xcaret" },
|
|
2053
|
-
month.label
|
|
1921
|
+
h("span", { className: "mneme-xcaret" }, h(Icon, { name: open ? "chevronDown" : "chevronRight", size: 12 })),
|
|
1922
|
+
h("span", null, month.label),
|
|
1923
|
+
h("span", { className: "mneme-xmonthcount" }, String(month.count))
|
|
2054
1924
|
),
|
|
2055
|
-
|
|
1925
|
+
open && month.days.map((day) =>
|
|
2056
1926
|
h("div", { key: day.key },
|
|
2057
1927
|
h("button", {
|
|
2058
1928
|
className: "mneme-xday",
|
|
2059
1929
|
"aria-expanded": String(!collapsed[day.key]),
|
|
2060
1930
|
onClick: () => setCollapsed((c) => ({ ...c, [day.key]: !c[day.key] }))
|
|
2061
1931
|
},
|
|
2062
|
-
h("span", { className: "mneme-xcaret" }, collapsed[day.key] ? "
|
|
1932
|
+
h("span", { className: "mneme-xcaret" }, h(Icon, { name: collapsed[day.key] ? "chevronRight" : "chevronDown", size: 11 })),
|
|
2063
1933
|
day.label
|
|
2064
1934
|
),
|
|
2065
1935
|
!collapsed[day.key] && day.items.map((m) => {
|
|
@@ -2078,12 +1948,19 @@ window.__ModuleLoader__.load({
|
|
|
2078
1948
|
h("span", { className: "mneme-xname" }, m.title || m.content?.slice(0, 40))
|
|
2079
1949
|
);
|
|
2080
1950
|
})
|
|
2081
|
-
)
|
|
2082
|
-
)
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
1951
|
+
))
|
|
1952
|
+
);
|
|
1953
|
+
}),
|
|
1954
|
+
h("div", { ref: moreRef, className: "mneme-xmore" },
|
|
1955
|
+
canLoadMore
|
|
1956
|
+
? h("button", { className: "mneme-footbtn", disabled: loadingMore, onClick: loadMore },
|
|
1957
|
+
loadingMore ? "…" : `${t("memory.explorer.loadMore")}(${items.length}/${total})`)
|
|
1958
|
+
: (!loading && visible.length > 0 && !q && !remoteItems
|
|
1959
|
+
? h("span", { className: "mneme-xcount" }, `${items.length} / ${total}`)
|
|
1960
|
+
: null)
|
|
1961
|
+
)
|
|
1962
|
+
),
|
|
1963
|
+
h("div", { className: "mneme-xdetail" },
|
|
2087
1964
|
selected
|
|
2088
1965
|
? h(react.Fragment, { key: selected.id },
|
|
2089
1966
|
h("div", { className: "mneme-xdinner" },
|
|
@@ -2099,64 +1976,46 @@ window.__ModuleLoader__.load({
|
|
|
2099
1976
|
h("span", { title: formatDate(selected.created_at) }, `${t("memory.explorer.created")}: ${formatDateShort(selected.created_at)}`),
|
|
2100
1977
|
h("span", { title: formatDate(selected.updated_at) }, `${t("memory.explorer.updated")}: ${formatRelativeTime(selected.updated_at, t)}`)
|
|
2101
1978
|
),
|
|
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
|
|
1979
|
+
Array.isArray(selected.tags) && selected.tags.length > 0 && h("div", { className: "mneme-xdmeta" },
|
|
1980
|
+
h("span", null, `${t("memory.explorer.tags")}: ${selected.tags.join(" · ")}`)
|
|
2143
1981
|
),
|
|
1982
|
+
h("div", { className: "mneme-xdcontent" }, selected.content),
|
|
2144
1983
|
h("div", { className: "mneme-xdactions" },
|
|
2145
1984
|
h("button", { className: "mneme-footbtn", onClick: copyContent },
|
|
2146
|
-
copied ?
|
|
2147
|
-
|
|
2148
|
-
|
|
1985
|
+
h(Icon, { name: copied ? "check" : "copy", size: 12 }),
|
|
1986
|
+
copied ? t("memory.explorer.copied") : t("memory.explorer.copy")),
|
|
1987
|
+
// two-step delete: red outline arms, solid red commits
|
|
1988
|
+
confirmDelete
|
|
1989
|
+
? h(react.Fragment, null,
|
|
1990
|
+
h("button", {
|
|
1991
|
+
className: "mneme-btn mneme-btndangerconfirm",
|
|
1992
|
+
disabled: deleting,
|
|
1993
|
+
onClick: deleteSelected
|
|
1994
|
+
}, t("memory.explorer.confirmDelete")),
|
|
1995
|
+
h("button", {
|
|
1996
|
+
className: "mneme-btn",
|
|
1997
|
+
onClick: () => setConfirmDelete(false)
|
|
1998
|
+
}, t("memory.explorer.cancel"))
|
|
1999
|
+
)
|
|
2000
|
+
: h("button", {
|
|
2001
|
+
className: "mneme-btn mneme-btndanger",
|
|
2002
|
+
onClick: () => setConfirmDelete(true)
|
|
2003
|
+
}, t("memory.explorer.delete")),
|
|
2004
|
+
deleteError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)" } },
|
|
2005
|
+
t("memory.explorer.deleteFailed"))
|
|
2006
|
+
)
|
|
2149
2007
|
)
|
|
2150
2008
|
)
|
|
2151
|
-
: h("div", { className: "mneme-xempty" },
|
|
2009
|
+
: h("div", { className: "mneme-xempty" },
|
|
2010
|
+
deleteMsg && h("div", { className: "mneme-saved", style: { marginBottom: 6 } }, t("memory.explorer.deleted")),
|
|
2011
|
+
t("memory.explorer.emptyDetail"))
|
|
2152
2012
|
)
|
|
2153
2013
|
)
|
|
2154
2014
|
),
|
|
2155
|
-
view === "
|
|
2156
|
-
view === "
|
|
2157
|
-
view === "
|
|
2158
|
-
|
|
2159
|
-
h("div", { className: "mneme-xsettings-inner" }, h(SettingsContent, { t }))
|
|
2015
|
+
view === "entity" && h(EntityPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
|
|
2016
|
+
view === "status" && h(StatusPanel, { t }),
|
|
2017
|
+
view === "settings" && h("div", { className: "mneme-set" },
|
|
2018
|
+
h("div", { className: "mneme-set-inner" }, h(SettingsContent, { t }))
|
|
2160
2019
|
)
|
|
2161
2020
|
);
|
|
2162
2021
|
}
|
|
@@ -2167,22 +2026,7 @@ window.__ModuleLoader__.load({
|
|
|
2167
2026
|
// opens the full-viewport overlay instead — the library stays reachable
|
|
2168
2027
|
// from every conversation state.
|
|
2169
2028
|
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
2029
|
const [, setOpen] = useOverlayOpen();
|
|
2185
|
-
if (!visible) return null;
|
|
2186
2030
|
return h("button", {
|
|
2187
2031
|
type: "button",
|
|
2188
2032
|
className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",
|