memorix 1.0.7 → 1.0.9
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/CHANGELOG.md +73 -11
- package/README.md +469 -409
- package/README.zh-CN.md +477 -408
- package/TEAM.md +106 -0
- package/dist/cli/index.js +34462 -27684
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +983 -59
- package/dist/dashboard/static/index.html +24 -4
- package/dist/dashboard/static/style.css +663 -0
- package/dist/index.js +1692 -247
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +677 -0
- package/dist/sdk.js +19580 -0
- package/dist/sdk.js.map +1 -0
- package/dist/types.d.ts +12 -11
- package/dist/types.js +11 -10
- package/dist/types.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +684 -0
- package/docs/AI_CONTEXT.md +18 -0
- package/docs/API_REFERENCE.md +687 -0
- package/docs/ARCHITECTURE.md +488 -0
- package/docs/CLOUD_SYNC_AND_MULTI_AGENT_RESEARCH.md +470 -0
- package/docs/CONFIGURATION.md +265 -0
- package/docs/DESIGN_DECISIONS.md +358 -0
- package/docs/DEVELOPMENT.md +317 -0
- package/docs/DOCKER.md +138 -0
- package/docs/GIT_MEMORY.md +221 -0
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +149 -0
- package/docs/MEMORY_FORMATION_PIPELINE.md +224 -0
- package/docs/MODULES.md +383 -0
- package/docs/PERFORMANCE.md +64 -0
- package/docs/README.md +79 -0
- package/docs/SETUP.md +617 -0
- package/docs/hooks-architecture.md +108 -0
- package/package.json +24 -23
|
@@ -96,11 +96,11 @@ const i18n = {
|
|
|
96
96
|
noRetentionData: 'No Retention Data',
|
|
97
97
|
noRetentionDesc: 'Store observations to see memory retention scores',
|
|
98
98
|
|
|
99
|
-
// Team
|
|
100
|
-
teamTitle: '
|
|
101
|
-
teamSubtitle: '
|
|
102
|
-
teamNoData: '
|
|
103
|
-
teamNoDataHint: '
|
|
99
|
+
// Team -> Autonomous Agent Team
|
|
100
|
+
teamTitle: 'Agent Team',
|
|
101
|
+
teamSubtitle: 'Autonomous CLI agents, tasks, locks, and handoffs',
|
|
102
|
+
teamNoData: 'No autonomous agent workflow activity yet',
|
|
103
|
+
teamNoDataHint: 'Use the CLI to start an autonomous workflow or inspect team state.',
|
|
104
104
|
teamActiveAgents: 'Active Agents',
|
|
105
105
|
teamLockedFiles: 'Locked Files',
|
|
106
106
|
teamTasks: 'Tasks',
|
|
@@ -147,6 +147,28 @@ const i18n = {
|
|
|
147
147
|
recentGitMemories: 'Recent Git Memories',
|
|
148
148
|
commit: 'Commit',
|
|
149
149
|
created: 'Created',
|
|
150
|
+
knowledgeTitle: 'Knowledge Base',
|
|
151
|
+
knowledgeSubtitle: 'Project-scoped engineering knowledge distilled from durable memory',
|
|
152
|
+
knowledgeObservationsUsed: 'Observations Used',
|
|
153
|
+
knowledgeMiniSkillsUsed: 'Mini-skills Used',
|
|
154
|
+
knowledgeRefs: 'Refs',
|
|
155
|
+
knowledgeGenerated: 'Generated',
|
|
156
|
+
knowledgeQuickJump: 'Sections',
|
|
157
|
+
knowledgeNoItems: 'No entries in this section yet',
|
|
158
|
+
knowledgeNoItemsDesc: 'This section will fill in as durable project knowledge is stored.',
|
|
159
|
+
knowledgeUnavailable: 'Knowledge Base Unavailable',
|
|
160
|
+
knowledgeUnavailableDesc: 'Could not load /api/knowledge for the selected project.',
|
|
161
|
+
knowledgeEmpty: 'No Knowledge Base Entries',
|
|
162
|
+
knowledgeEmptyDesc: 'Store durable observations or promote mini-skills to populate this read-only project wiki.',
|
|
163
|
+
knowledgeProvenance: 'Provenance',
|
|
164
|
+
knowledgeEntry: 'entry',
|
|
165
|
+
knowledgeEntries: 'entries',
|
|
166
|
+
knowledgeSectionProjectOverview: 'Project Overview',
|
|
167
|
+
knowledgeSectionCoreDecisions: 'Core Decisions',
|
|
168
|
+
knowledgeSectionOperationalKnowledge: 'Operational Knowledge',
|
|
169
|
+
knowledgeSectionKnownGotchas: 'Known Gotchas',
|
|
170
|
+
knowledgeSectionGitBackedFacts: 'Git-backed Facts',
|
|
171
|
+
knowledgeSectionPromotedSkills: 'Promoted Skills',
|
|
150
172
|
|
|
151
173
|
// Config
|
|
152
174
|
configTitle: 'Config Provenance',
|
|
@@ -205,17 +227,17 @@ const i18n = {
|
|
|
205
227
|
projectUnresolved: 'Unresolved',
|
|
206
228
|
projectUnresolvedDesc: 'No project bound — select a project from the switcher',
|
|
207
229
|
projectResolved: 'Resolved',
|
|
208
|
-
projectScopeProject: 'Project
|
|
230
|
+
projectScopeProject: 'Project Agent Team',
|
|
209
231
|
projectScopeGlobal: 'All Projects',
|
|
210
|
-
projectScopeProjectDesc: '
|
|
211
|
-
projectScopeGlobalDesc: '
|
|
232
|
+
projectScopeProjectDesc: 'Autonomous agents and tasks for this project',
|
|
233
|
+
projectScopeGlobalDesc: 'Autonomous agents and tasks across projects',
|
|
212
234
|
|
|
213
235
|
// Team (additional)
|
|
214
236
|
teamMessages: 'Messages',
|
|
215
237
|
teamAllRead: 'All read',
|
|
216
238
|
teamUnread: 'unread',
|
|
217
|
-
teamNoAgentsProject: 'No agents
|
|
218
|
-
teamNoAgentsGlobal: 'No agents in any scope',
|
|
239
|
+
teamNoAgentsProject: 'No autonomous agents recorded for this project',
|
|
240
|
+
teamNoAgentsGlobal: 'No autonomous agents recorded in any scope',
|
|
219
241
|
teamNoFilesLocked: 'No files locked',
|
|
220
242
|
teamNoTasksCreated: 'No tasks created',
|
|
221
243
|
teamPending: 'pending',
|
|
@@ -235,13 +257,13 @@ const i18n = {
|
|
|
235
257
|
teamRecentCount: 'recent',
|
|
236
258
|
teamHistoricalCount: 'historical',
|
|
237
259
|
teamHistoricalTotal: 'Historical total',
|
|
238
|
-
teamHistoricalHint: 'Inactive for more than 7 days. Not current
|
|
260
|
+
teamHistoricalHint: 'Inactive for more than 7 days. Not current autonomous agents.',
|
|
239
261
|
teamRecentHint: 'Inactive, last seen within 7 days.',
|
|
240
262
|
teamShowHistorical: 'Show historical',
|
|
241
263
|
teamHideHistorical: 'Hide historical',
|
|
242
|
-
teamNoActiveNow: 'No active agents right now',
|
|
243
|
-
teamNoRecent: 'No agents seen in the last 7 days',
|
|
244
|
-
teamSummaryHint: '
|
|
264
|
+
teamNoActiveNow: 'No active autonomous agents right now',
|
|
265
|
+
teamNoRecent: 'No autonomous agents seen in the last 7 days',
|
|
266
|
+
teamSummaryHint: 'Only explicit autonomous agent identities appear here. Historical rows are collapsed by default.',
|
|
245
267
|
// Resume area
|
|
246
268
|
resumeTitle: 'Continue This Project',
|
|
247
269
|
resumeDesc: 'Pick up where you left off',
|
|
@@ -314,6 +336,34 @@ const i18n = {
|
|
|
314
336
|
graphTopToBottom: 'Top → Bottom',
|
|
315
337
|
graphMore: 'more',
|
|
316
338
|
|
|
339
|
+
// Knowledge Graph (semantic)
|
|
340
|
+
kgTitle: 'Knowledge Graph',
|
|
341
|
+
kgSubtitle: 'Semantic knowledge topology — section clusters, evidence links, provenance',
|
|
342
|
+
kgNodes: 'nodes',
|
|
343
|
+
kgEdges: 'edges',
|
|
344
|
+
kgClusters: 'clusters',
|
|
345
|
+
kgNoData: 'No Knowledge Graph Data',
|
|
346
|
+
kgNoDataDesc: 'Store durable observations or promote mini-skills to generate a semantic knowledge graph.',
|
|
347
|
+
kgClusterFilter: 'Section Cluster',
|
|
348
|
+
kgEdgeTypeFilter: 'Edge Type',
|
|
349
|
+
kgEdgeSupports: 'supports',
|
|
350
|
+
kgEdgeRelatesTo: 'relates to',
|
|
351
|
+
kgEdgeMentions: 'mentions',
|
|
352
|
+
kgEdgeDerivedFrom: 'derived from',
|
|
353
|
+
kgInspectorSummary: 'Summary',
|
|
354
|
+
kgInspectorProvenance: 'Provenance',
|
|
355
|
+
kgInspectorSection: 'Section',
|
|
356
|
+
kgInspectorEntity: 'Entity',
|
|
357
|
+
kgInspectorEvidence: 'Evidence',
|
|
358
|
+
kgInspectorRelatedEdges: 'Related Edges',
|
|
359
|
+
kgInspectorNoEdges: 'No edges',
|
|
360
|
+
kgDataMode: 'Data Source',
|
|
361
|
+
kgModeSemantic: 'Semantic KG',
|
|
362
|
+
kgModeEntity: 'Entity Graph',
|
|
363
|
+
kgViewMode: 'View Mode',
|
|
364
|
+
kgFocused: 'Focused',
|
|
365
|
+
kgFullGraph: 'Full Graph',
|
|
366
|
+
|
|
317
367
|
// Identity (additional)
|
|
318
368
|
identityCurrentProject: 'Current Project',
|
|
319
369
|
identityHistoricalProjects: 'Historical Projects',
|
|
@@ -333,25 +383,27 @@ const i18n = {
|
|
|
333
383
|
// Nav tooltips + labels
|
|
334
384
|
navDashboard: 'Dashboard',
|
|
335
385
|
navGitMemory: 'Git Memory',
|
|
386
|
+
navKnowledge: 'Knowledge Base',
|
|
336
387
|
navGraph: 'Knowledge Graph',
|
|
337
388
|
navObservations: 'Observations',
|
|
338
389
|
navRetention: 'Retention',
|
|
339
390
|
navConfig: 'Config',
|
|
340
391
|
navIdentity: 'Identity',
|
|
341
392
|
navSessions: 'Sessions',
|
|
342
|
-
navTeam: '
|
|
393
|
+
navTeam: 'Agent Team',
|
|
343
394
|
navLabelDashboard: 'Overview',
|
|
344
395
|
navLabelGitMemory: 'Git Memory',
|
|
396
|
+
navLabelKnowledge: 'Knowledge',
|
|
345
397
|
navLabelGraph: 'Graph',
|
|
346
398
|
navLabelObservations: 'Observations',
|
|
347
399
|
navLabelRetention: 'Retention',
|
|
348
400
|
navLabelConfig: 'Config',
|
|
349
401
|
navLabelIdentity: 'Identity',
|
|
350
402
|
navLabelSessions: 'Sessions',
|
|
351
|
-
navLabelTeam: '
|
|
403
|
+
navLabelTeam: 'Agent Team',
|
|
352
404
|
sectionCore: 'CORE',
|
|
353
405
|
sectionHealth: 'HEALTH',
|
|
354
|
-
sectionCollaboration: '
|
|
406
|
+
sectionCollaboration: 'AGENTS',
|
|
355
407
|
themeDark: 'Dark',
|
|
356
408
|
themeLight: 'Light',
|
|
357
409
|
loading: 'Loading...',
|
|
@@ -360,8 +412,8 @@ const i18n = {
|
|
|
360
412
|
// Mode banner
|
|
361
413
|
modeStandalone: 'Standalone',
|
|
362
414
|
modeControlPlane: 'Control Plane',
|
|
363
|
-
modeStandaloneHint: '
|
|
364
|
-
modeControlPlaneHint: '
|
|
415
|
+
modeStandaloneHint: 'Standalone dashboard - memory and read-only agent team state',
|
|
416
|
+
modeControlPlaneHint: 'HTTP control plane - shared MCP access and live dashboard',
|
|
365
417
|
modeBannerProject: 'Project',
|
|
366
418
|
modeBannerMcp: 'MCP',
|
|
367
419
|
|
|
@@ -466,16 +518,16 @@ const i18n = {
|
|
|
466
518
|
noRetentionData: '暂无衰减数据',
|
|
467
519
|
noRetentionDesc: '存储观察记录以查看记忆衰减分数',
|
|
468
520
|
|
|
469
|
-
// Team
|
|
470
|
-
teamTitle: '
|
|
471
|
-
teamSubtitle: '
|
|
472
|
-
teamNoData: '
|
|
473
|
-
teamNoDataHint: '
|
|
521
|
+
// Team -> 自主 Agent 团队
|
|
522
|
+
teamTitle: 'Agent 团队',
|
|
523
|
+
teamSubtitle: '自主 CLI agents、任务、文件锁和交接状态',
|
|
524
|
+
teamNoData: '暂无自主 agent 工作流活动',
|
|
525
|
+
teamNoDataHint: '使用 CLI 启动自主工作流,或查看团队任务状态。',
|
|
474
526
|
teamActiveAgents: '活跃 Agent',
|
|
475
527
|
teamLockedFiles: '锁定文件',
|
|
476
528
|
teamTasks: '任务',
|
|
477
529
|
teamAvailable: '可领取',
|
|
478
|
-
teamAgents: 'Agent
|
|
530
|
+
teamAgents: 'Agent',
|
|
479
531
|
teamLocks: '文件锁',
|
|
480
532
|
teamTaskBoard: '任务看板',
|
|
481
533
|
// Resume area
|
|
@@ -506,17 +558,39 @@ const i18n = {
|
|
|
506
558
|
|
|
507
559
|
// Git Memory
|
|
508
560
|
gitMemoryTitle: 'Git 记忆',
|
|
509
|
-
gitMemorySubtitle: '来自 git 提交的记忆 —
|
|
561
|
+
gitMemorySubtitle: '来自 git 提交的记忆 — 真实来源且不可变',
|
|
510
562
|
totalGitMemories: 'Git 记忆总数',
|
|
511
|
-
uniqueCommits: '
|
|
563
|
+
uniqueCommits: '唯一提交',
|
|
512
564
|
typeCoverage: '类型覆盖',
|
|
513
565
|
noGitMemory: '暂无 Git 记忆',
|
|
514
566
|
noGitMemoryDesc: '使用以下命令安装 post-commit hook: memorix git-hook-install',
|
|
515
567
|
noGitMemoriesYet: '暂无 Git 记忆',
|
|
516
568
|
noGitMemoriesHint: '安装 post-commit hook 以自动捕获 git 记忆:',
|
|
517
|
-
recentGitMemories: '
|
|
569
|
+
recentGitMemories: '最近 Git 记忆',
|
|
518
570
|
commit: '提交',
|
|
519
571
|
created: '创建时间',
|
|
572
|
+
knowledgeTitle: '知识库',
|
|
573
|
+
knowledgeSubtitle: '从持久项目记忆中提炼出的工程知识',
|
|
574
|
+
knowledgeObservationsUsed: '使用的观察',
|
|
575
|
+
knowledgeMiniSkillsUsed: '使用的 Mini-skills',
|
|
576
|
+
knowledgeRefs: '引用',
|
|
577
|
+
knowledgeGenerated: '生成于',
|
|
578
|
+
knowledgeQuickJump: '章节',
|
|
579
|
+
knowledgeNoItems: '本章节暂无条目',
|
|
580
|
+
knowledgeNoItemsDesc: '当项目中出现持久知识后,本章节会自动填充。',
|
|
581
|
+
knowledgeUnavailable: '知识库不可用',
|
|
582
|
+
knowledgeUnavailableDesc: '无法为所选项目加载 /api/knowledge。',
|
|
583
|
+
knowledgeEmpty: '暂无知识库条目',
|
|
584
|
+
knowledgeEmptyDesc: '存储持久观察或提升 mini-skill 后,此只读项目 wiki 会自动填充。',
|
|
585
|
+
knowledgeProvenance: '来源',
|
|
586
|
+
knowledgeEntry: '条',
|
|
587
|
+
knowledgeEntries: '条',
|
|
588
|
+
knowledgeSectionProjectOverview: '项目概览',
|
|
589
|
+
knowledgeSectionCoreDecisions: '核心决策',
|
|
590
|
+
knowledgeSectionOperationalKnowledge: '操作知识',
|
|
591
|
+
knowledgeSectionKnownGotchas: '已知陷阱',
|
|
592
|
+
knowledgeSectionGitBackedFacts: 'Git 事实',
|
|
593
|
+
knowledgeSectionPromotedSkills: '提升技能',
|
|
520
594
|
|
|
521
595
|
// Config
|
|
522
596
|
configTitle: '配置溯源',
|
|
@@ -575,17 +649,17 @@ const i18n = {
|
|
|
575
649
|
projectUnresolved: '未绑定',
|
|
576
650
|
projectUnresolvedDesc: '无项目绑定 — 请从切换器选择项目',
|
|
577
651
|
projectResolved: '已绑定',
|
|
578
|
-
projectScopeProject: '
|
|
652
|
+
projectScopeProject: '项目 Agent 团队',
|
|
579
653
|
projectScopeGlobal: '所有项目',
|
|
580
|
-
projectScopeProjectDesc: '
|
|
581
|
-
projectScopeGlobalDesc: '
|
|
654
|
+
projectScopeProjectDesc: '当前项目中的自主 agents 和任务',
|
|
655
|
+
projectScopeGlobalDesc: '所有项目中的自主 agents 和任务',
|
|
582
656
|
|
|
583
657
|
// Team (additional)
|
|
584
658
|
teamMessages: '消息',
|
|
585
659
|
teamAllRead: '全部已读',
|
|
586
660
|
teamUnread: '未读',
|
|
587
|
-
teamNoAgentsProject: '
|
|
588
|
-
teamNoAgentsGlobal: '
|
|
661
|
+
teamNoAgentsProject: '当前项目暂无自主 agent 记录',
|
|
662
|
+
teamNoAgentsGlobal: '任何范围均无自主 agent 记录',
|
|
589
663
|
teamNoFilesLocked: '无文件锁定',
|
|
590
664
|
teamNoTasksCreated: '无已创建任务',
|
|
591
665
|
teamPending: '待处理',
|
|
@@ -605,13 +679,13 @@ const i18n = {
|
|
|
605
679
|
teamRecentCount: '近期',
|
|
606
680
|
teamHistoricalCount: '历史',
|
|
607
681
|
teamHistoricalTotal: '历史累计',
|
|
608
|
-
teamHistoricalHint: '超过 7
|
|
682
|
+
teamHistoricalHint: '超过 7 天无活动,非当前自主 agent',
|
|
609
683
|
teamRecentHint: '未活跃,最近 7 天内有过心跳',
|
|
610
684
|
teamShowHistorical: '显示历史',
|
|
611
685
|
teamHideHistorical: '隐藏历史',
|
|
612
|
-
teamNoActiveNow: '
|
|
613
|
-
teamNoRecent: '最近 7
|
|
614
|
-
teamSummaryHint: '
|
|
686
|
+
teamNoActiveNow: '当前无活跃自主 agent',
|
|
687
|
+
teamNoRecent: '最近 7 天无自主 agent 活动',
|
|
688
|
+
teamSummaryHint: '这里只显示显式自主 agent 身份。历史数据默认折叠。',
|
|
615
689
|
// Resume area
|
|
616
690
|
resumeTitle: '继续此项目',
|
|
617
691
|
resumeDesc: '从上次中断处继续',
|
|
@@ -689,6 +763,34 @@ const i18n = {
|
|
|
689
763
|
graphTopToBottom: '从上到下',
|
|
690
764
|
graphMore: '更多',
|
|
691
765
|
|
|
766
|
+
// Knowledge Graph (semantic)
|
|
767
|
+
kgTitle: '知识图谱',
|
|
768
|
+
kgSubtitle: '语义知识拓扑 — 分区聚类、证据关联、溯源',
|
|
769
|
+
kgNodes: '个节点',
|
|
770
|
+
kgEdges: '条边',
|
|
771
|
+
kgClusters: '个聚类',
|
|
772
|
+
kgNoData: '暂无知识图谱数据',
|
|
773
|
+
kgNoDataDesc: '存储持久观察或提升迷你技能以生成语义知识图谱。',
|
|
774
|
+
kgClusterFilter: '分区聚类',
|
|
775
|
+
kgEdgeTypeFilter: '边类型',
|
|
776
|
+
kgEdgeSupports: '支撑',
|
|
777
|
+
kgEdgeRelatesTo: '关联',
|
|
778
|
+
kgEdgeMentions: '提及',
|
|
779
|
+
kgEdgeDerivedFrom: '派生自',
|
|
780
|
+
kgInspectorSummary: '摘要',
|
|
781
|
+
kgInspectorProvenance: '溯源',
|
|
782
|
+
kgInspectorSection: '分区',
|
|
783
|
+
kgInspectorEntity: '实体',
|
|
784
|
+
kgInspectorEvidence: '证据',
|
|
785
|
+
kgInspectorRelatedEdges: '相关边',
|
|
786
|
+
kgInspectorNoEdges: '无边',
|
|
787
|
+
kgDataMode: '数据源',
|
|
788
|
+
kgModeSemantic: '语义图谱',
|
|
789
|
+
kgModeEntity: '实体图谱',
|
|
790
|
+
kgViewMode: '视图模式',
|
|
791
|
+
kgFocused: '聚焦',
|
|
792
|
+
kgFullGraph: '全量',
|
|
793
|
+
|
|
692
794
|
// Identity (additional)
|
|
693
795
|
identityCurrentProject: '当前项目',
|
|
694
796
|
identityHistoricalProjects: '历史项目',
|
|
@@ -708,25 +810,27 @@ const i18n = {
|
|
|
708
810
|
// Nav tooltips
|
|
709
811
|
navDashboard: '仪表盘',
|
|
710
812
|
navGitMemory: 'Git 记忆',
|
|
813
|
+
navKnowledge: '知识库',
|
|
711
814
|
navGraph: '知识图谱',
|
|
712
815
|
navObservations: '观察记录',
|
|
713
816
|
navRetention: '记忆衰减',
|
|
714
817
|
navConfig: '配置溯源',
|
|
715
818
|
navIdentity: '身份健康',
|
|
716
819
|
navSessions: '会话',
|
|
717
|
-
navTeam: '
|
|
820
|
+
navTeam: 'Agent 团队',
|
|
718
821
|
navLabelDashboard: '概览',
|
|
719
822
|
navLabelGitMemory: 'Git 记忆',
|
|
823
|
+
navLabelKnowledge: '知识库',
|
|
720
824
|
navLabelGraph: '图谱',
|
|
721
825
|
navLabelObservations: '观察',
|
|
722
826
|
navLabelRetention: '衰减',
|
|
723
827
|
navLabelConfig: '配置',
|
|
724
828
|
navLabelIdentity: '身份',
|
|
725
829
|
navLabelSessions: '会话',
|
|
726
|
-
navLabelTeam: '
|
|
830
|
+
navLabelTeam: 'Agent 团队',
|
|
727
831
|
sectionCore: '核心',
|
|
728
832
|
sectionHealth: '健康',
|
|
729
|
-
sectionCollaboration: '
|
|
833
|
+
sectionCollaboration: 'Agents',
|
|
730
834
|
themeDark: '深色',
|
|
731
835
|
themeLight: '浅色',
|
|
732
836
|
loading: '加载中...',
|
|
@@ -735,8 +839,8 @@ const i18n = {
|
|
|
735
839
|
// Mode banner
|
|
736
840
|
modeStandalone: '独立模式',
|
|
737
841
|
modeControlPlane: '控制平面',
|
|
738
|
-
modeStandaloneHint: '
|
|
739
|
-
modeControlPlaneHint: '
|
|
842
|
+
modeStandaloneHint: '独立看板 - 记忆与只读 Agent 团队状态',
|
|
843
|
+
modeControlPlaneHint: 'HTTP 控制平面 - 共享 MCP 接入与实时看板',
|
|
740
844
|
modeBannerProject: '项目',
|
|
741
845
|
modeBannerMcp: 'MCP',
|
|
742
846
|
|
|
@@ -764,6 +868,14 @@ function t(key) {
|
|
|
764
868
|
return (i18n[currentLang] && i18n[currentLang][key]) || i18n.en[key] || key;
|
|
765
869
|
}
|
|
766
870
|
|
|
871
|
+
function onDomReady(fn) {
|
|
872
|
+
if (document.readyState === 'loading') {
|
|
873
|
+
document.addEventListener('DOMContentLoaded', fn, { once: true });
|
|
874
|
+
} else {
|
|
875
|
+
fn();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
767
879
|
function setLang(lang) {
|
|
768
880
|
currentLang = lang;
|
|
769
881
|
localStorage.setItem('memorix-lang', lang);
|
|
@@ -774,8 +886,8 @@ function setLang(lang) {
|
|
|
774
886
|
if (label) label.textContent = lang === 'en' ? '中文' : 'EN';
|
|
775
887
|
|
|
776
888
|
// Update nav tooltips + labels
|
|
777
|
-
const tooltipMap = { dashboard: 'navDashboard', 'git-memory': 'navGitMemory', graph: 'navGraph', observations: 'navObservations', retention: 'navRetention', config: 'navConfig', identity: 'navIdentity', sessions: 'navSessions', team: 'navTeam' };
|
|
778
|
-
const labelMap = { dashboard: 'navLabelDashboard', 'git-memory': 'navLabelGitMemory', graph: 'navLabelGraph', observations: 'navLabelObservations', retention: 'navLabelRetention', config: 'navLabelConfig', identity: 'navLabelIdentity', sessions: 'navLabelSessions', team: 'navLabelTeam' };
|
|
889
|
+
const tooltipMap = { dashboard: 'navDashboard', 'git-memory': 'navGitMemory', knowledge: 'navKnowledge', graph: 'navGraph', observations: 'navObservations', retention: 'navRetention', config: 'navConfig', identity: 'navIdentity', sessions: 'navSessions', team: 'navTeam' };
|
|
890
|
+
const labelMap = { dashboard: 'navLabelDashboard', 'git-memory': 'navLabelGitMemory', knowledge: 'navLabelKnowledge', graph: 'navLabelGraph', observations: 'navLabelObservations', retention: 'navLabelRetention', config: 'navLabelConfig', identity: 'navLabelIdentity', sessions: 'navLabelSessions', team: 'navLabelTeam' };
|
|
779
891
|
document.querySelectorAll('.nav-btn').forEach(b => {
|
|
780
892
|
const page = b.dataset.page;
|
|
781
893
|
if (page && tooltipMap[page]) b.title = t(tooltipMap[page]);
|
|
@@ -810,7 +922,7 @@ function setLang(lang) {
|
|
|
810
922
|
}
|
|
811
923
|
|
|
812
924
|
// Init lang toggle button
|
|
813
|
-
|
|
925
|
+
onDomReady(() => {
|
|
814
926
|
const btn = document.getElementById('lang-toggle');
|
|
815
927
|
const label = document.getElementById('lang-label');
|
|
816
928
|
if (label) label.textContent = currentLang === 'en' ? '中文' : 'EN';
|
|
@@ -853,7 +965,7 @@ function applyTheme(theme) {
|
|
|
853
965
|
// Apply saved theme immediately
|
|
854
966
|
applyTheme(currentTheme);
|
|
855
967
|
|
|
856
|
-
|
|
968
|
+
onDomReady(() => {
|
|
857
969
|
const themeBtn = document.getElementById('theme-toggle');
|
|
858
970
|
if (themeBtn) {
|
|
859
971
|
themeBtn.addEventListener('click', () => {
|
|
@@ -866,7 +978,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
866
978
|
// Router & Navigation
|
|
867
979
|
// ============================================================
|
|
868
980
|
|
|
869
|
-
const pages = ['dashboard', 'git-memory', 'graph', 'observations', 'retention', 'config', 'identity', 'sessions', 'team'];
|
|
981
|
+
const pages = ['dashboard', 'git-memory', 'knowledge', 'graph', 'observations', 'retention', 'config', 'identity', 'sessions', 'team'];
|
|
870
982
|
let currentPage = 'dashboard';
|
|
871
983
|
|
|
872
984
|
function navigate(page) {
|
|
@@ -1151,7 +1263,7 @@ async function initProjectSwitcher() {
|
|
|
1151
1263
|
}
|
|
1152
1264
|
}
|
|
1153
1265
|
|
|
1154
|
-
|
|
1266
|
+
onDomReady(() => {
|
|
1155
1267
|
initProjectSwitcher();
|
|
1156
1268
|
});
|
|
1157
1269
|
|
|
@@ -1167,6 +1279,7 @@ async function loadPage(page) {
|
|
|
1167
1279
|
switch (page) {
|
|
1168
1280
|
case 'dashboard': await loadDashboard(); break;
|
|
1169
1281
|
case 'git-memory': await loadGitMemory(); break;
|
|
1282
|
+
case 'knowledge': await loadKnowledge(); break;
|
|
1170
1283
|
case 'graph': await loadGraph(); break;
|
|
1171
1284
|
case 'observations': await loadObservations(); break;
|
|
1172
1285
|
case 'retention': await loadRetention(); break;
|
|
@@ -1332,7 +1445,7 @@ async function loadDashboard() {
|
|
|
1332
1445
|
<div style="flex: 1;">
|
|
1333
1446
|
${typeEntries.map(([type, count]) => `
|
|
1334
1447
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
1335
|
-
<span style="width: 18px; text-align: center; font-size: 13px;">${typeIcons[type] || '
|
|
1448
|
+
<span style="width: 18px; text-align: center; font-size: 13px;">${typeIcons[type] || '[UNKNOWN]'}</span>
|
|
1336
1449
|
<span style="width: 110px; font-size: 11px; color: var(--text-secondary);">${type}</span>
|
|
1337
1450
|
<div style="flex: 1; height: 5px; background: rgba(128,128,128,0.1); border-radius: 3px; overflow: hidden;">
|
|
1338
1451
|
<div style="width: ${(count / maxTypeCount) * 100}%; height: 100%; background: var(--type-${type}, var(--accent-cyan)); border-radius: 3px;"></div>
|
|
@@ -1417,6 +1530,201 @@ function renderPieChart(canvasId, entries, icons) {
|
|
|
1417
1530
|
ctx.fillText('total', cx, cy + 10);
|
|
1418
1531
|
}
|
|
1419
1532
|
|
|
1533
|
+
async function loadKnowledge() {
|
|
1534
|
+
const container = document.getElementById('page-knowledge');
|
|
1535
|
+
container.innerHTML = '<div class="loading knowledge-loading"><div class="spinner"></div><div class="knowledge-loading-text">' + t('loading') + '</div></div>';
|
|
1536
|
+
|
|
1537
|
+
const kb = await api('knowledge');
|
|
1538
|
+
if (!kb) {
|
|
1539
|
+
container.innerHTML = `
|
|
1540
|
+
<div class="page-header">
|
|
1541
|
+
<h1 class="page-title">${t('knowledgeTitle')}</h1>
|
|
1542
|
+
<p class="page-subtitle">${t('knowledgeSubtitle')}</p>
|
|
1543
|
+
</div>
|
|
1544
|
+
<div class="panel knowledge-error-panel">
|
|
1545
|
+
<div class="panel-body">
|
|
1546
|
+
<div class="knowledge-state-icon"><span class="iconify" data-icon="lucide:book-x"></span></div>
|
|
1547
|
+
<div class="empty-state-title">${t('knowledgeUnavailable')}</div>
|
|
1548
|
+
<div class="empty-state-desc">${t('knowledgeUnavailableDesc')}</div>
|
|
1549
|
+
<button class="team-refresh-btn" id="knowledge-retry-btn" style="margin:16px auto 0;">${t('teamRefresh')}</button>
|
|
1550
|
+
</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
`;
|
|
1553
|
+
const retry = document.getElementById('knowledge-retry-btn');
|
|
1554
|
+
if (retry) retry.addEventListener('click', () => {
|
|
1555
|
+
delete loaded.knowledge;
|
|
1556
|
+
loadPage('knowledge');
|
|
1557
|
+
});
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const sections = normalizeKnowledgeSections(kb.sections || []);
|
|
1562
|
+
const totalItems = sections.reduce((sum, section) => sum + (section.items || []).length, 0);
|
|
1563
|
+
const stats = kb.stats || { observationsUsed: 0, miniSkillsUsed: 0, refs: 0 };
|
|
1564
|
+
const overviewSection = sections.find(s => s.id === 'project-overview');
|
|
1565
|
+
const mainSections = sections.filter(s => s.id !== 'project-overview');
|
|
1566
|
+
|
|
1567
|
+
container.innerHTML = `
|
|
1568
|
+
<div class="knowledge-page-shell">
|
|
1569
|
+
<div class="knowledge-topbar">
|
|
1570
|
+
<div class="page-header knowledge-header">
|
|
1571
|
+
<div class="knowledge-title-block">
|
|
1572
|
+
<h1 class="page-title">${escapeHtml(t('knowledgeTitle'))}</h1>
|
|
1573
|
+
<p class="page-subtitle">${t('knowledgeSubtitle')}</p>
|
|
1574
|
+
<div class="knowledge-project-line" title="${escapeHtml(kb.projectId || '')}">${escapeHtml(kb.projectId || '')}</div>
|
|
1575
|
+
</div>
|
|
1576
|
+
<div class="knowledge-generated">
|
|
1577
|
+
<span>${t('knowledgeGenerated')}</span>
|
|
1578
|
+
<strong>${escapeHtml(formatTime(kb.generatedAt))}</strong>
|
|
1579
|
+
</div>
|
|
1580
|
+
</div>
|
|
1581
|
+
|
|
1582
|
+
<div class="knowledge-stats-grid">
|
|
1583
|
+
<div class="stat-card" data-accent="purple">
|
|
1584
|
+
<div class="stat-label">${t('knowledgeObservationsUsed')}</div>
|
|
1585
|
+
<div class="stat-value">${stats.observationsUsed || 0}</div>
|
|
1586
|
+
</div>
|
|
1587
|
+
<div class="stat-card" data-accent="cyan">
|
|
1588
|
+
<div class="stat-label">${t('knowledgeMiniSkillsUsed')}</div>
|
|
1589
|
+
<div class="stat-value">${stats.miniSkillsUsed || 0}</div>
|
|
1590
|
+
</div>
|
|
1591
|
+
<div class="stat-card" data-accent="green">
|
|
1592
|
+
<div class="stat-label">${t('knowledgeRefs')}</div>
|
|
1593
|
+
<div class="stat-value">${stats.refs || 0}</div>
|
|
1594
|
+
</div>
|
|
1595
|
+
</div>
|
|
1596
|
+
|
|
1597
|
+
<nav class="knowledge-jump" aria-label="${t('knowledgeQuickJump')}">
|
|
1598
|
+
<span class="knowledge-jump-label">${t('knowledgeQuickJump')}</span>
|
|
1599
|
+
${sections.map(section => `<a href="#${knowledgeAnchor(section.id)}" class="knowledge-jump-chip">${escapeHtml(section.title)} <span>${(section.items || []).length}</span></a>`).join('')}
|
|
1600
|
+
</nav>
|
|
1601
|
+
|
|
1602
|
+
${overviewSection ? renderKnowledgeOverviewCard(overviewSection) : ''}
|
|
1603
|
+
</div>
|
|
1604
|
+
|
|
1605
|
+
${totalItems === 0 ? `
|
|
1606
|
+
<div class="panel knowledge-empty-overview">
|
|
1607
|
+
<div class="panel-body">
|
|
1608
|
+
<div class="knowledge-state-icon"><span class="iconify" data-icon="lucide:book-open"></span></div>
|
|
1609
|
+
<div class="empty-state-title">${t('knowledgeEmpty')}</div>
|
|
1610
|
+
<div class="empty-state-desc">${t('knowledgeEmptyDesc')}</div>
|
|
1611
|
+
</div>
|
|
1612
|
+
</div>
|
|
1613
|
+
` : `
|
|
1614
|
+
<div class="knowledge-sections-region">
|
|
1615
|
+
${mainSections.map(renderKnowledgeSection).join('')}
|
|
1616
|
+
</div>
|
|
1617
|
+
`}
|
|
1618
|
+
</div>
|
|
1619
|
+
`;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function normalizeKnowledgeSections(sections) {
|
|
1623
|
+
const byId = new Map(sections.map(section => [section.id, section]));
|
|
1624
|
+
const order = [
|
|
1625
|
+
['project-overview', 'knowledgeSectionProjectOverview'],
|
|
1626
|
+
['core-decisions', 'knowledgeSectionCoreDecisions'],
|
|
1627
|
+
['operational-knowledge', 'knowledgeSectionOperationalKnowledge'],
|
|
1628
|
+
['known-gotchas', 'knowledgeSectionKnownGotchas'],
|
|
1629
|
+
['git-backed-facts', 'knowledgeSectionGitBackedFacts'],
|
|
1630
|
+
['promoted-skills', 'knowledgeSectionPromotedSkills'],
|
|
1631
|
+
];
|
|
1632
|
+
return order.map(([id, titleKey]) => {
|
|
1633
|
+
const section = byId.get(id);
|
|
1634
|
+
const localizedTitle = t(titleKey);
|
|
1635
|
+
if (section) return { ...section, title: localizedTitle };
|
|
1636
|
+
return { id, title: localizedTitle, items: [], empty: true };
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function knowledgeEntryLabel(count) {
|
|
1641
|
+
return count === 1 ? t('knowledgeEntry') : t('knowledgeEntries');
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function renderKnowledgeOverviewCard(section) {
|
|
1645
|
+
const item = (section.items || [])[0];
|
|
1646
|
+
if (!item) {
|
|
1647
|
+
return `
|
|
1648
|
+
<section class="knowledge-overview-card knowledge-overview-card--empty" id="${knowledgeAnchor(section.id)}">
|
|
1649
|
+
<div class="knowledge-overview-meta">
|
|
1650
|
+
<span class="knowledge-overview-label">${escapeHtml(section.title)}</span>
|
|
1651
|
+
<span class="knowledge-section-meta">0 ${t('knowledgeEntries')}</span>
|
|
1652
|
+
</div>
|
|
1653
|
+
<div class="knowledge-overview-body">
|
|
1654
|
+
<p class="knowledge-summary">${t('knowledgeNoItemsDesc')}</p>
|
|
1655
|
+
</div>
|
|
1656
|
+
</section>
|
|
1657
|
+
`;
|
|
1658
|
+
}
|
|
1659
|
+
return `
|
|
1660
|
+
<section class="knowledge-overview-card" id="${knowledgeAnchor(section.id)}">
|
|
1661
|
+
<div class="knowledge-overview-meta">
|
|
1662
|
+
<span class="knowledge-overview-label">${escapeHtml(section.title)}</span>
|
|
1663
|
+
<span class="knowledge-overview-title" title="${escapeHtml(item.title || '')}">${escapeHtml(item.title || t('untitled'))}</span>
|
|
1664
|
+
</div>
|
|
1665
|
+
<div class="knowledge-overview-body">
|
|
1666
|
+
<p class="knowledge-summary knowledge-summary--overview">${escapeHtml(item.summary || '')}</p>
|
|
1667
|
+
<div class="knowledge-ref-list">${renderKnowledgeRefs(item.refs || [])}</div>
|
|
1668
|
+
</div>
|
|
1669
|
+
</section>
|
|
1670
|
+
`;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function renderKnowledgeSection(section) {
|
|
1674
|
+
const items = section.items || [];
|
|
1675
|
+
return `
|
|
1676
|
+
<section class="panel knowledge-section" id="${knowledgeAnchor(section.id)}">
|
|
1677
|
+
<div class="panel-header knowledge-section-header">
|
|
1678
|
+
<span class="panel-title">${escapeHtml(section.title)}</span>
|
|
1679
|
+
<span class="knowledge-section-meta">${items.length} ${knowledgeEntryLabel(items.length)}</span>
|
|
1680
|
+
</div>
|
|
1681
|
+
<div class="panel-body knowledge-section-body">
|
|
1682
|
+
${items.length === 0 ? renderKnowledgeEmptySection() : items.map(renderKnowledgeItem).join('')}
|
|
1683
|
+
</div>
|
|
1684
|
+
</section>
|
|
1685
|
+
`;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function renderKnowledgeItem(item) {
|
|
1689
|
+
const refs = item.refs || [];
|
|
1690
|
+
return `
|
|
1691
|
+
<article class="knowledge-item">
|
|
1692
|
+
<div class="knowledge-item-head">
|
|
1693
|
+
<h3 title="${escapeHtml(item.title || t('untitled'))}">${escapeHtml(item.title || t('untitled'))}</h3>
|
|
1694
|
+
<span class="type-badge" data-type="${escapeHtml(item.type || 'unknown')}">${escapeHtml(item.type || t('unknown'))}</span>
|
|
1695
|
+
</div>
|
|
1696
|
+
${item.entityName ? `<div class="knowledge-entity" title="${escapeHtml(item.entityName)}">${escapeHtml(item.entityName)}</div>` : ''}
|
|
1697
|
+
<p class="knowledge-summary">${escapeHtml(item.summary || '')}</p>
|
|
1698
|
+
${refs.length > 0 ? `<div class="knowledge-ref-list">${renderKnowledgeRefs(refs)}</div>` : ''}
|
|
1699
|
+
</article>
|
|
1700
|
+
`;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function renderKnowledgeRefs(refs) {
|
|
1704
|
+
if (refs.length === 0) return '<span class="knowledge-ref-empty">—</span>';
|
|
1705
|
+
return refs.map(ref => {
|
|
1706
|
+
const kind = ref.kind || 'observation';
|
|
1707
|
+
const title = ref.title ? ` title="${escapeHtml(ref.title)}"` : '';
|
|
1708
|
+
return `<span class="knowledge-ref-chip" data-kind="${escapeHtml(kind)}"${title}>${escapeHtml(ref.id || kind)}</span>`;
|
|
1709
|
+
}).join('');
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function renderKnowledgeEmptySection() {
|
|
1713
|
+
return `
|
|
1714
|
+
<div class="knowledge-section-empty">
|
|
1715
|
+
<span class="iconify" data-icon="lucide:circle-dashed"></span>
|
|
1716
|
+
<div>
|
|
1717
|
+
<strong>${t('knowledgeNoItems')}</strong>
|
|
1718
|
+
<span>${t('knowledgeNoItemsDesc')}</span>
|
|
1719
|
+
</div>
|
|
1720
|
+
</div>
|
|
1721
|
+
`;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function knowledgeAnchor(id) {
|
|
1725
|
+
return `knowledge-${String(id || 'section').replace(/[^a-z0-9_-]/gi, '-')}`;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1420
1728
|
// ============================================================
|
|
1421
1729
|
// Memory Topology Explorer — Cytoscape.js + Dagre
|
|
1422
1730
|
// Focused topology default, not full graph dump
|
|
@@ -1428,9 +1736,21 @@ async function loadGraph() {
|
|
|
1428
1736
|
const container = document.getElementById('page-graph');
|
|
1429
1737
|
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
1430
1738
|
|
|
1739
|
+
// Try semantic Knowledge Graph first
|
|
1740
|
+
try {
|
|
1741
|
+
const kg = await api('knowledge-graph');
|
|
1742
|
+
if (kg && kg.nodes && kg.nodes.length > 0) {
|
|
1743
|
+
renderSemanticGraph(kg);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
} catch (_e) {
|
|
1747
|
+
// Fallback to entity graph
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// Fallback: entity/relation graph
|
|
1431
1751
|
const graph = await api('graph');
|
|
1432
1752
|
if (!graph || (graph.entities.length === 0 && graph.relations.length === 0)) {
|
|
1433
|
-
container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:network" style="font-size:36px;"></span>', t('
|
|
1753
|
+
container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:network" style="font-size:36px;"></span>', t('kgNoData'), t('kgNoDataDesc'));
|
|
1434
1754
|
return;
|
|
1435
1755
|
}
|
|
1436
1756
|
|
|
@@ -1482,6 +1802,610 @@ async function loadGraph() {
|
|
|
1482
1802
|
renderGraph(graph);
|
|
1483
1803
|
}
|
|
1484
1804
|
|
|
1805
|
+
// ============================================================
|
|
1806
|
+
// Semantic Knowledge Graph Renderer — Apache ECharts 5
|
|
1807
|
+
// Force layout, section categories, evidence-based edges
|
|
1808
|
+
// ============================================================
|
|
1809
|
+
|
|
1810
|
+
function renderSemanticGraph(kg) {
|
|
1811
|
+
const container = document.getElementById('page-graph');
|
|
1812
|
+
|
|
1813
|
+
// Section color palette (vivid for both light/dark themes)
|
|
1814
|
+
const sectionPalette = {
|
|
1815
|
+
'core-decisions': '#7B9FD9',
|
|
1816
|
+
'operational-knowledge': '#7CC598',
|
|
1817
|
+
'known-gotchas': '#E08585',
|
|
1818
|
+
'git-backed-facts': '#56D6A6',
|
|
1819
|
+
'promoted-skills': '#C9A8FF',
|
|
1820
|
+
};
|
|
1821
|
+
const defaultSectionColor = '#A893C2';
|
|
1822
|
+
|
|
1823
|
+
function getSectionColor(sectionId) {
|
|
1824
|
+
return sectionPalette[sectionId] || defaultSectionColor;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Edge type styles — solid hex + opacity controlled by G6 strokeOpacity
|
|
1828
|
+
const edgeStyleMap = {
|
|
1829
|
+
'supports': { color: '#69F0AE', arrow: true, dash: false, label: t('kgEdgeSupports') },
|
|
1830
|
+
'relates_to': { color: '#80D8FF', arrow: false, dash: [4, 4], label: t('kgEdgeRelatesTo') },
|
|
1831
|
+
'mentions': { color: '#D0BCFF', arrow: true, dash: false, label: t('kgEdgeMentions') },
|
|
1832
|
+
'derived_from': { color: '#FFB74D', arrow: true, dash: [6, 3], label: t('kgEdgeDerivedFrom') },
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1835
|
+
// Section label i18n map (matches knowledgeSection* keys)
|
|
1836
|
+
const sectionI18nKeys = {
|
|
1837
|
+
'core-decisions': 'knowledgeSectionCoreDecisions',
|
|
1838
|
+
'operational-knowledge': 'knowledgeSectionOperationalKnowledge',
|
|
1839
|
+
'known-gotchas': 'knowledgeSectionKnownGotchas',
|
|
1840
|
+
'git-backed-facts': 'knowledgeSectionGitBackedFacts',
|
|
1841
|
+
'promoted-skills': 'knowledgeSectionPromotedSkills',
|
|
1842
|
+
};
|
|
1843
|
+
function sectionLabel(sectionId) {
|
|
1844
|
+
const key = sectionI18nKeys[sectionId];
|
|
1845
|
+
return key ? t(key) : sectionId;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// State
|
|
1849
|
+
let activeSections = new Set((kg.clusters || []).map(c => c.sectionId));
|
|
1850
|
+
let activeEdgeTypes = new Set(Object.keys(edgeStyleMap).filter(type => type !== 'relates_to'));
|
|
1851
|
+
let selectedNodeId = null;
|
|
1852
|
+
let echartsInstance = null;
|
|
1853
|
+
let echartsResizeObserver = null;
|
|
1854
|
+
let focusedMode = true; // default: show top-N nodes only
|
|
1855
|
+
const FOCUSED_TOP_N = 40; // max nodes in focused view
|
|
1856
|
+
const MAX_EDGES_PER_NODE_FOCUSED = 4; // cap per-node edges (top-K by priority) to keep graph readable
|
|
1857
|
+
const FOCUSED_EDGE_BUDGET = 120; // hard visual budget for first-render readability
|
|
1858
|
+
|
|
1859
|
+
function isLight() { return document.documentElement.getAttribute('data-theme') === 'light'; }
|
|
1860
|
+
|
|
1861
|
+
// Compute degree (edge count) for each node
|
|
1862
|
+
const nodeDegreeMap = new Map();
|
|
1863
|
+
for (const edge of kg.edges) {
|
|
1864
|
+
nodeDegreeMap.set(edge.source, (nodeDegreeMap.get(edge.source) || 0) + 1);
|
|
1865
|
+
nodeDegreeMap.set(edge.target, (nodeDegreeMap.get(edge.target) || 0) + 1);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// Section ordering for stable category index
|
|
1869
|
+
const SECTION_ORDER = ['core-decisions', 'operational-knowledge', 'known-gotchas', 'git-backed-facts', 'promoted-skills'];
|
|
1870
|
+
|
|
1871
|
+
// Build ECharts graph data: { categories, nodes, links }
|
|
1872
|
+
function buildEChartsData() {
|
|
1873
|
+
// Determine which nodes to show
|
|
1874
|
+
let visibleNodes = kg.nodes.filter(n => activeSections.has(n.sectionId));
|
|
1875
|
+
if (focusedMode && visibleNodes.length > FOCUSED_TOP_N) {
|
|
1876
|
+
visibleNodes.sort((a, b) => {
|
|
1877
|
+
const scoreA = (a.evidenceCount || 0) + (nodeDegreeMap.get(a.id) || 0) * 2;
|
|
1878
|
+
const scoreB = (b.evidenceCount || 0) + (nodeDegreeMap.get(b.id) || 0) * 2;
|
|
1879
|
+
return scoreB - scoreA;
|
|
1880
|
+
});
|
|
1881
|
+
visibleNodes = visibleNodes.slice(0, FOCUSED_TOP_N);
|
|
1882
|
+
}
|
|
1883
|
+
const visibleNodeIds = new Set(visibleNodes.map(n => n.id));
|
|
1884
|
+
|
|
1885
|
+
// Categories (one per section, ECharts uses index reference from nodes)
|
|
1886
|
+
const categories = SECTION_ORDER.map(sectionId => ({
|
|
1887
|
+
name: sectionLabel(sectionId),
|
|
1888
|
+
sectionId,
|
|
1889
|
+
itemStyle: { color: getSectionColor(sectionId) },
|
|
1890
|
+
}));
|
|
1891
|
+
const categoryIndexBySection = Object.fromEntries(SECTION_ORDER.map((s, i) => [s, i]));
|
|
1892
|
+
const visibleBySection = new Map(SECTION_ORDER.map(sectionId => [sectionId, []]));
|
|
1893
|
+
visibleNodes.forEach(node => {
|
|
1894
|
+
const group = visibleBySection.get(node.sectionId) || [];
|
|
1895
|
+
group.push(node);
|
|
1896
|
+
visibleBySection.set(node.sectionId, group);
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
const sectionCenters = {
|
|
1900
|
+
'core-decisions': { x: -360, y: -170 },
|
|
1901
|
+
'operational-knowledge': { x: 0, y: 0 },
|
|
1902
|
+
'known-gotchas': { x: -340, y: 190 },
|
|
1903
|
+
'git-backed-facts': { x: 360, y: -150 },
|
|
1904
|
+
'promoted-skills': { x: 350, y: 190 },
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
function stableNodePosition(node, index) {
|
|
1908
|
+
const group = visibleBySection.get(node.sectionId) || [];
|
|
1909
|
+
const total = Math.max(1, group.length);
|
|
1910
|
+
const center = sectionCenters[node.sectionId] || { x: 0, y: 0 };
|
|
1911
|
+
const ring = Math.floor(index / 10);
|
|
1912
|
+
const ringIndex = index % 10;
|
|
1913
|
+
const radius = total === 1 ? 0 : 34 + ring * 42;
|
|
1914
|
+
const angleStep = (Math.PI * 2) / Math.min(10, total);
|
|
1915
|
+
const angleOffset = SECTION_ORDER.indexOf(node.sectionId) * 0.55;
|
|
1916
|
+
const angle = ringIndex * angleStep + angleOffset;
|
|
1917
|
+
return {
|
|
1918
|
+
x: Math.round(center.x + Math.cos(angle) * radius),
|
|
1919
|
+
y: Math.round(center.y + Math.sin(angle) * radius),
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// Nodes (stable coordinates; dragging a node must not trigger global force rotation)
|
|
1924
|
+
const nodes = visibleNodes.map(node => {
|
|
1925
|
+
const symbolSize = Math.max(14, Math.min(10 + Math.sqrt(node.evidenceCount || 1) * 4, 32));
|
|
1926
|
+
const sectionIndex = (visibleBySection.get(node.sectionId) || []).findIndex(n => n.id === node.id);
|
|
1927
|
+
const pos = stableNodePosition(node, Math.max(0, sectionIndex));
|
|
1928
|
+
return {
|
|
1929
|
+
id: node.id,
|
|
1930
|
+
name: node.label.length > 28 ? node.label.slice(0, 26) + '\u2026' : node.label,
|
|
1931
|
+
x: pos.x,
|
|
1932
|
+
y: pos.y,
|
|
1933
|
+
category: categoryIndexBySection[node.sectionId] ?? 0,
|
|
1934
|
+
symbolSize,
|
|
1935
|
+
value: node.evidenceCount || 0,
|
|
1936
|
+
sectionId: node.sectionId,
|
|
1937
|
+
nodeType: node.nodeType,
|
|
1938
|
+
entityName: node.entityName || '',
|
|
1939
|
+
evidenceCount: node.evidenceCount || 0,
|
|
1940
|
+
summary: node.summary || '',
|
|
1941
|
+
refs: node.refs || [],
|
|
1942
|
+
fullLabel: node.label,
|
|
1943
|
+
};
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
// Collect candidate edges (both endpoints visible + active type)
|
|
1947
|
+
const edgePriority = { supports: 3, derived_from: 2, mentions: 1, relates_to: 0 };
|
|
1948
|
+
const candidateEdges = kg.edges.filter(e =>
|
|
1949
|
+
activeEdgeTypes.has(e.edgeType) &&
|
|
1950
|
+
visibleNodeIds.has(e.source) &&
|
|
1951
|
+
visibleNodeIds.has(e.target),
|
|
1952
|
+
);
|
|
1953
|
+
|
|
1954
|
+
// In focused mode, cap each node's edges to top-K strongest by type priority
|
|
1955
|
+
let finalEdges = candidateEdges;
|
|
1956
|
+
if (focusedMode && candidateEdges.length > 0) {
|
|
1957
|
+
// Sort all candidates by priority descending (stronger types first)
|
|
1958
|
+
const sorted = [...candidateEdges].sort(
|
|
1959
|
+
(a, b) => (edgePriority[b.edgeType] ?? 0) - (edgePriority[a.edgeType] ?? 0),
|
|
1960
|
+
);
|
|
1961
|
+
const perNodeCount = new Map();
|
|
1962
|
+
finalEdges = [];
|
|
1963
|
+
for (const e of sorted) {
|
|
1964
|
+
if (finalEdges.length >= FOCUSED_EDGE_BUDGET) break;
|
|
1965
|
+
const sCount = perNodeCount.get(e.source) || 0;
|
|
1966
|
+
const tCount = perNodeCount.get(e.target) || 0;
|
|
1967
|
+
if (sCount >= MAX_EDGES_PER_NODE_FOCUSED || tCount >= MAX_EDGES_PER_NODE_FOCUSED) continue;
|
|
1968
|
+
perNodeCount.set(e.source, sCount + 1);
|
|
1969
|
+
perNodeCount.set(e.target, tCount + 1);
|
|
1970
|
+
finalEdges.push(e);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// Build ECharts link objects
|
|
1975
|
+
const links = finalEdges.map(edge => {
|
|
1976
|
+
const style = edgeStyleMap[edge.edgeType] || {};
|
|
1977
|
+
return {
|
|
1978
|
+
id: edge.id,
|
|
1979
|
+
source: edge.source,
|
|
1980
|
+
target: edge.target,
|
|
1981
|
+
edgeType: edge.edgeType,
|
|
1982
|
+
edgeLabel: style.label || edge.edgeType,
|
|
1983
|
+
lineStyle: {
|
|
1984
|
+
color: style.color || '#999',
|
|
1985
|
+
opacity: 0.5,
|
|
1986
|
+
width: 1.1,
|
|
1987
|
+
curveness: 0.12,
|
|
1988
|
+
type: Array.isArray(style.dash) ? 'dashed' : 'solid',
|
|
1989
|
+
},
|
|
1990
|
+
symbol: style.arrow ? ['none', 'arrow'] : ['none', 'none'],
|
|
1991
|
+
symbolSize: [4, 7],
|
|
1992
|
+
};
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
return { categories, nodes, links };
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
let lastRenderedCounts = { nodes: 0, edges: 0 };
|
|
1999
|
+
|
|
2000
|
+
// Render the page shell
|
|
2001
|
+
container.innerHTML = `
|
|
2002
|
+
<div class="kg-page-shell">
|
|
2003
|
+
<div class="page-header">
|
|
2004
|
+
<h1 class="page-title">${t('kgTitle')}</h1>
|
|
2005
|
+
<p class="page-subtitle">${t('kgSubtitle')}</p>
|
|
2006
|
+
</div>
|
|
2007
|
+
<div class="graph-layout">
|
|
2008
|
+
<div class="graph-filter-panel" id="kg-filter-panel"></div>
|
|
2009
|
+
<div id="graph-container">
|
|
2010
|
+
<div id="echarts-mount" style="width:100%;height:100%;"></div>
|
|
2011
|
+
<div class="graph-status-bar">
|
|
2012
|
+
<span class="graph-status-item" id="gs-nodes"></span>
|
|
2013
|
+
<span class="graph-status-item" id="gs-edges"></span>
|
|
2014
|
+
<span class="graph-status-item" id="gs-clusters"></span>
|
|
2015
|
+
<div class="graph-zoom-controls">
|
|
2016
|
+
<button class="graph-zoom-btn" id="gz-out">\u2212</button>
|
|
2017
|
+
<button class="graph-zoom-btn" id="gz-fit">\u2B21</button>
|
|
2018
|
+
<button class="graph-zoom-btn" id="gz-in">+</button>
|
|
2019
|
+
</div>
|
|
2020
|
+
</div>
|
|
2021
|
+
</div>
|
|
2022
|
+
<div class="graph-inspector" id="graph-inspector">
|
|
2023
|
+
<div class="gi-empty"><div class="gi-empty-icon">\u2B21</div>${t('graphSelectNode')}</div>
|
|
2024
|
+
</div>
|
|
2025
|
+
</div>
|
|
2026
|
+
</div>
|
|
2027
|
+
`;
|
|
2028
|
+
|
|
2029
|
+
function initEChartsGraph() {
|
|
2030
|
+
const mountEl = document.getElementById('echarts-mount');
|
|
2031
|
+
if (!mountEl || typeof echarts === 'undefined') return;
|
|
2032
|
+
|
|
2033
|
+
// Ensure container has dimensions before init
|
|
2034
|
+
if (mountEl.clientWidth === 0 || mountEl.clientHeight === 0) {
|
|
2035
|
+
// Defer until layout finishes
|
|
2036
|
+
requestAnimationFrame(() => initEChartsGraph());
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Dispose previous instance if any
|
|
2041
|
+
if (echartsInstance) {
|
|
2042
|
+
try { echartsInstance.dispose(); } catch (_) { /* noop */ }
|
|
2043
|
+
echartsInstance = null;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
const data = buildEChartsData();
|
|
2047
|
+
lastRenderedCounts = { nodes: data.nodes.length, edges: data.links.length };
|
|
2048
|
+
const light = isLight();
|
|
2049
|
+
const labelColor = light ? '#1C1B1F' : '#E6E1E5';
|
|
2050
|
+
const labelMutedColor = light ? '#666' : '#999';
|
|
2051
|
+
const tooltipBg = light ? 'rgba(255,255,255,0.96)' : 'rgba(20,20,28,0.96)';
|
|
2052
|
+
const tooltipBorder = light ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.12)';
|
|
2053
|
+
|
|
2054
|
+
echartsInstance = echarts.init(mountEl, null, { renderer: 'canvas' });
|
|
2055
|
+
|
|
2056
|
+
const wrap = (txt) => String(txt ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2057
|
+
|
|
2058
|
+
const option = {
|
|
2059
|
+
backgroundColor: 'transparent',
|
|
2060
|
+
tooltip: {
|
|
2061
|
+
trigger: 'item',
|
|
2062
|
+
backgroundColor: tooltipBg,
|
|
2063
|
+
borderColor: tooltipBorder,
|
|
2064
|
+
borderWidth: 1,
|
|
2065
|
+
textStyle: { color: labelColor, fontSize: 12, fontFamily: 'Inter, system-ui, sans-serif' },
|
|
2066
|
+
extraCssText: 'box-shadow: 0 4px 16px rgba(0,0,0,0.18); border-radius: 8px; padding: 10px 12px; max-width: 320px;',
|
|
2067
|
+
formatter: (params) => {
|
|
2068
|
+
if (params.dataType === 'node') {
|
|
2069
|
+
const d = params.data;
|
|
2070
|
+
const summary = d.summary ? String(d.summary).slice(0, 160) : '';
|
|
2071
|
+
return (
|
|
2072
|
+
`<div style="font-weight:600;font-size:13px;margin-bottom:4px;line-height:1.35;">${wrap(d.fullLabel || d.name)}</div>` +
|
|
2073
|
+
`<div style="font-size:11px;color:${labelMutedColor};margin-bottom:6px;">${wrap(d.nodeType || '')}${d.entityName ? ' \u00b7 ' + wrap(d.entityName) : ''}</div>` +
|
|
2074
|
+
`<div style="font-size:11px;color:${labelMutedColor};">${d.evidenceCount || 0} ${wrap(t('kgInspectorEvidence'))}</div>` +
|
|
2075
|
+
(summary ? `<div style="font-size:11.5px;line-height:1.5;margin-top:8px;color:${labelColor};opacity:0.85;">${wrap(summary)}\u2026</div>` : '')
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
if (params.dataType === 'edge') {
|
|
2079
|
+
return `<div style="font-size:12px;">${wrap(params.data.edgeLabel || params.data.edgeType)}</div>`;
|
|
2080
|
+
}
|
|
2081
|
+
return '';
|
|
2082
|
+
},
|
|
2083
|
+
},
|
|
2084
|
+
legend: [{
|
|
2085
|
+
data: data.categories.map(c => c.name),
|
|
2086
|
+
textStyle: { color: labelColor, fontSize: 11 },
|
|
2087
|
+
top: 8,
|
|
2088
|
+
right: 12,
|
|
2089
|
+
itemWidth: 10,
|
|
2090
|
+
itemHeight: 10,
|
|
2091
|
+
itemGap: 12,
|
|
2092
|
+
icon: 'circle',
|
|
2093
|
+
selectedMode: false,
|
|
2094
|
+
}],
|
|
2095
|
+
animationDuration: 500,
|
|
2096
|
+
animationEasingUpdate: 'quinticInOut',
|
|
2097
|
+
series: [{
|
|
2098
|
+
type: 'graph',
|
|
2099
|
+
layout: 'none',
|
|
2100
|
+
roam: true,
|
|
2101
|
+
draggable: true,
|
|
2102
|
+
zoom: 1,
|
|
2103
|
+
scaleLimit: { min: 0.2, max: 4 },
|
|
2104
|
+
edgeSymbol: ['none', 'arrow'],
|
|
2105
|
+
edgeSymbolSize: [0, 8],
|
|
2106
|
+
categories: data.categories,
|
|
2107
|
+
data: data.nodes,
|
|
2108
|
+
edges: data.links,
|
|
2109
|
+
label: {
|
|
2110
|
+
show: true,
|
|
2111
|
+
position: 'right',
|
|
2112
|
+
formatter: '{b}',
|
|
2113
|
+
color: labelColor,
|
|
2114
|
+
fontSize: 11,
|
|
2115
|
+
fontFamily: 'Inter, system-ui, sans-serif',
|
|
2116
|
+
backgroundColor: light ? 'rgba(255,255,255,0.82)' : 'rgba(15,15,23,0.72)',
|
|
2117
|
+
padding: [2, 5],
|
|
2118
|
+
borderRadius: 3,
|
|
2119
|
+
},
|
|
2120
|
+
labelLayout: { hideOverlap: true, moveOverlap: 'shiftY' },
|
|
2121
|
+
itemStyle: {
|
|
2122
|
+
opacity: 0.92,
|
|
2123
|
+
borderColor: light ? 'rgba(0,0,0,0.22)' : 'rgba(255,255,255,0.2)',
|
|
2124
|
+
borderWidth: 1,
|
|
2125
|
+
shadowColor: light ? 'rgba(0,0,0,0.18)' : 'rgba(0,0,0,0.45)',
|
|
2126
|
+
shadowBlur: 8,
|
|
2127
|
+
},
|
|
2128
|
+
emphasis: {
|
|
2129
|
+
focus: 'adjacency',
|
|
2130
|
+
label: { show: true, fontWeight: 700, fontSize: 12 },
|
|
2131
|
+
itemStyle: { borderColor: light ? '#6750A4' : '#D0BCFF', borderWidth: 2 },
|
|
2132
|
+
lineStyle: { width: 2.5, opacity: 1 },
|
|
2133
|
+
},
|
|
2134
|
+
blur: {
|
|
2135
|
+
itemStyle: { opacity: 0.18 },
|
|
2136
|
+
label: { opacity: 0.25 },
|
|
2137
|
+
lineStyle: { opacity: 0.06 },
|
|
2138
|
+
},
|
|
2139
|
+
lineStyle: { opacity: 0.55, curveness: 0.15, width: 1.2 },
|
|
2140
|
+
}],
|
|
2141
|
+
};
|
|
2142
|
+
|
|
2143
|
+
echartsInstance.setOption(option);
|
|
2144
|
+
|
|
2145
|
+
// Click handlers — use ECharts event API (auto-cleaned on dispose) instead of zrender
|
|
2146
|
+
echartsInstance.on('click', (params) => {
|
|
2147
|
+
if (params.dataType === 'node') {
|
|
2148
|
+
const id = params.data?.id;
|
|
2149
|
+
if (id) {
|
|
2150
|
+
selectedNodeId = id;
|
|
2151
|
+
showKGInspector(id);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
// Click on blank canvas deselects (ECharts emits click with empty target at chart level).
|
|
2156
|
+
// Use `click` event at series level only; do NOT intercept zrender events so roam/pan works.
|
|
2157
|
+
// User reported pan-on-empty-canvas not working when zr click was intercepted.
|
|
2158
|
+
|
|
2159
|
+
// Resize observer
|
|
2160
|
+
if (echartsResizeObserver) {
|
|
2161
|
+
try { echartsResizeObserver.disconnect(); } catch (_) { /* noop */ }
|
|
2162
|
+
}
|
|
2163
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
2164
|
+
echartsResizeObserver = new ResizeObserver(() => {
|
|
2165
|
+
if (echartsInstance) echartsInstance.resize();
|
|
2166
|
+
});
|
|
2167
|
+
echartsResizeObserver.observe(mountEl);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
window._kgEChart = echartsInstance;
|
|
2171
|
+
window._kgShowInspector = showKGInspector;
|
|
2172
|
+
|
|
2173
|
+
updateStatusBar();
|
|
2174
|
+
bindZoomControls();
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function bindZoomControls() {
|
|
2178
|
+
const outBtn = document.getElementById('gz-out');
|
|
2179
|
+
const fitBtn = document.getElementById('gz-fit');
|
|
2180
|
+
const inBtn = document.getElementById('gz-in');
|
|
2181
|
+
const setZoom = (factor) => {
|
|
2182
|
+
if (!echartsInstance) return;
|
|
2183
|
+
const opt = echartsInstance.getOption();
|
|
2184
|
+
const cur = (opt.series && opt.series[0] && opt.series[0].zoom) || 1;
|
|
2185
|
+
echartsInstance.setOption({ series: [{ zoom: Math.min(4, Math.max(0.2, cur * factor)) }] });
|
|
2186
|
+
};
|
|
2187
|
+
if (outBtn) outBtn.onclick = () => setZoom(0.75);
|
|
2188
|
+
if (fitBtn) fitBtn.onclick = () => {
|
|
2189
|
+
if (echartsInstance) echartsInstance.setOption({ series: [{ zoom: 1, center: null }] });
|
|
2190
|
+
};
|
|
2191
|
+
if (inBtn) inBtn.onclick = () => setZoom(1.35);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
function updateStatusBar() {
|
|
2195
|
+
const gsNodes = document.getElementById('gs-nodes');
|
|
2196
|
+
const gsEdges = document.getElementById('gs-edges');
|
|
2197
|
+
const gsClusters = document.getElementById('gs-clusters');
|
|
2198
|
+
const shownNodes = focusedMode ? Math.min(kg.nodes.length, FOCUSED_TOP_N) : kg.nodes.length;
|
|
2199
|
+
const shownLabel = focusedMode && kg.nodes.length > FOCUSED_TOP_N ? `${shownNodes}/${kg.nodes.length}` : `${kg.nodes.length}`;
|
|
2200
|
+
if (gsNodes) gsNodes.textContent = `${shownLabel} ${t('kgNodes')}`;
|
|
2201
|
+
const edgeLabel = lastRenderedCounts.edges < kg.edges.length
|
|
2202
|
+
? `${lastRenderedCounts.edges}/${kg.edges.length}`
|
|
2203
|
+
: `${kg.edges.length}`;
|
|
2204
|
+
if (gsEdges) gsEdges.textContent = `${edgeLabel} ${t('kgEdges')}`;
|
|
2205
|
+
if (gsClusters) gsClusters.textContent = `${(kg.clusters || []).length} ${t('kgClusters')}`;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// Inspector
|
|
2209
|
+
function showKGInspector(nodeId) {
|
|
2210
|
+
const inspector = document.getElementById('graph-inspector');
|
|
2211
|
+
if (!inspector) return;
|
|
2212
|
+
if (!nodeId) {
|
|
2213
|
+
inspector.innerHTML = '<div class="gi-empty"><div class="gi-empty-icon">\u2B21</div>' + t('graphSelectNode') + '</div>';
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const node = kg.nodes.find(n => n.id === nodeId);
|
|
2217
|
+
if (!node) {
|
|
2218
|
+
inspector.innerHTML = '<div class="gi-empty"><div class="gi-empty-icon">\u2B21</div>' + t('graphSelectNode') + '</div>';
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
const color = getSectionColor(node.sectionId);
|
|
2222
|
+
const relatedEdges = kg.edges.filter(e => e.source === nodeId || e.target === nodeId);
|
|
2223
|
+
|
|
2224
|
+
const refsHtml = (node.refs || []).length > 0
|
|
2225
|
+
? node.refs.map(r => `<span class="knowledge-ref-chip" data-kind="${escapeHtml(r.kind)}">${escapeHtml(r.id)}</span>`).join('')
|
|
2226
|
+
: '<span style="font-size:12px;color:var(--text-muted);">—</span>';
|
|
2227
|
+
|
|
2228
|
+
const edgeHtml = relatedEdges.length > 0
|
|
2229
|
+
? relatedEdges.map(e => {
|
|
2230
|
+
const dir = e.source === nodeId;
|
|
2231
|
+
const other = dir ? e.target : e.source;
|
|
2232
|
+
const otherNode = kg.nodes.find(n => n.id === other);
|
|
2233
|
+
const style = edgeStyleMap[e.edgeType] || {};
|
|
2234
|
+
return `<div class="gi-rel-item">
|
|
2235
|
+
<span class="gi-rel-arrow">${dir ? '\u2192' : '\u2190'}</span>
|
|
2236
|
+
<span class="gi-rel-type" style="color:${style.color || 'var(--text-muted)'}">${escapeHtml(style.label || e.edgeType)}</span>
|
|
2237
|
+
<span class="gi-rel-target" data-kg-nav="${escapeHtml(other)}">${escapeHtml(otherNode?.label || other)}</span>
|
|
2238
|
+
</div>`;
|
|
2239
|
+
}).join('')
|
|
2240
|
+
: '<div style="font-size:12px;color:var(--text-muted);font-style:italic;">' + t('kgInspectorNoEdges') + '</div>';
|
|
2241
|
+
|
|
2242
|
+
inspector.innerHTML = `
|
|
2243
|
+
<div class="gi-header">
|
|
2244
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
|
|
2245
|
+
<span style="width:10px;height:10px;border-radius:50%;background:${color};flex-shrink:0;"></span>
|
|
2246
|
+
<div class="gi-name">${escapeHtml(node.label)}</div>
|
|
2247
|
+
</div>
|
|
2248
|
+
<div class="gi-type">${escapeHtml(node.nodeType)}</div>
|
|
2249
|
+
</div>
|
|
2250
|
+
<div class="gi-stats">
|
|
2251
|
+
<div class="gi-stat"><div class="gi-stat-value">${node.evidenceCount}</div><div class="gi-stat-label">${t('kgInspectorEvidence')}</div></div>
|
|
2252
|
+
<div class="gi-stat"><div class="gi-stat-value">${relatedEdges.length}</div><div class="gi-stat-label">${t('kgInspectorRelatedEdges')}</div></div>
|
|
2253
|
+
</div>
|
|
2254
|
+
<div class="gi-section">
|
|
2255
|
+
<div class="gi-section-title">${t('kgInspectorSection')}</div>
|
|
2256
|
+
<div style="font-size:12px;color:${color};font-weight:500;">${escapeHtml(sectionLabel(node.sectionId))}</div>
|
|
2257
|
+
</div>
|
|
2258
|
+
${node.entityName ? `<div class="gi-section"><div class="gi-section-title">${t('kgInspectorEntity')}</div><div style="font-size:12px;color:var(--accent-cyan);font-family:var(--font-mono);">${escapeHtml(node.entityName)}</div></div>` : ''}
|
|
2259
|
+
<div class="gi-section">
|
|
2260
|
+
<div class="gi-section-title">${t('kgInspectorSummary')}</div>
|
|
2261
|
+
<div style="font-size:12px;color:var(--text-secondary);line-height:1.5;">${escapeHtml(node.summary || '—')}</div>
|
|
2262
|
+
</div>
|
|
2263
|
+
<div class="gi-section">
|
|
2264
|
+
<div class="gi-section-title">${t('kgInspectorProvenance')}</div>
|
|
2265
|
+
<div class="knowledge-ref-list">${refsHtml}</div>
|
|
2266
|
+
</div>
|
|
2267
|
+
<div class="gi-section">
|
|
2268
|
+
<div class="gi-section-title">${t('kgInspectorRelatedEdges')} <span class="gi-section-count">${relatedEdges.length}</span></div>
|
|
2269
|
+
${edgeHtml}
|
|
2270
|
+
</div>
|
|
2271
|
+
`;
|
|
2272
|
+
|
|
2273
|
+
inspector.querySelectorAll('[data-kg-nav]').forEach(el => {
|
|
2274
|
+
el.addEventListener('click', () => {
|
|
2275
|
+
const targetId = el.dataset.kgNav;
|
|
2276
|
+
if (echartsInstance) {
|
|
2277
|
+
try {
|
|
2278
|
+
echartsInstance.dispatchAction({ type: 'highlight', seriesIndex: 0, dataType: 'node', name: kg.nodes.find(n => n.id === targetId)?.label });
|
|
2279
|
+
} catch (_) { /* noop */ }
|
|
2280
|
+
}
|
|
2281
|
+
showKGInspector(targetId);
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// Filter panel
|
|
2287
|
+
function renderKGFilterPanel() {
|
|
2288
|
+
const panel = document.getElementById('kg-filter-panel');
|
|
2289
|
+
if (!panel) return;
|
|
2290
|
+
|
|
2291
|
+
const clusterEntries = (kg.clusters || []).map(c => [c.sectionId, c]);
|
|
2292
|
+
const clusterHtml = `
|
|
2293
|
+
<div class="gfp-section">
|
|
2294
|
+
<div class="gfp-label">${t('kgClusterFilter')}</div>
|
|
2295
|
+
<div class="gfp-radio-group">
|
|
2296
|
+
${clusterEntries.map(([id, cluster]) => `
|
|
2297
|
+
<button class="gfp-check${activeSections.has(id) ? ' active' : ''}" data-section-filter="${escapeHtml(id)}">
|
|
2298
|
+
<span class="gfp-check-box">\u2713</span>
|
|
2299
|
+
<span class="gfp-type-dot" style="background:${getSectionColor(id)}"></span>
|
|
2300
|
+
${escapeHtml(sectionLabel(cluster.sectionId))}
|
|
2301
|
+
<span class="gfp-check-count">${cluster.nodeCount}</span>
|
|
2302
|
+
</button>
|
|
2303
|
+
`).join('')}
|
|
2304
|
+
</div>
|
|
2305
|
+
</div>
|
|
2306
|
+
`;
|
|
2307
|
+
|
|
2308
|
+
const edgeTypeEntries = Object.entries(edgeStyleMap);
|
|
2309
|
+
const edgeTypeHtml = `
|
|
2310
|
+
<div class="gfp-section">
|
|
2311
|
+
<div class="gfp-label">${t('kgEdgeTypeFilter')}</div>
|
|
2312
|
+
<div class="gfp-radio-group">
|
|
2313
|
+
${edgeTypeEntries.map(([type, style]) => `
|
|
2314
|
+
<button class="gfp-check${activeEdgeTypes.has(type) ? ' active' : ''}" data-edge-type-filter="${escapeHtml(type)}">
|
|
2315
|
+
<span class="gfp-check-box">\u2713</span>
|
|
2316
|
+
<span style="color:${style.color};font-size:11px;">\u2192</span>
|
|
2317
|
+
${escapeHtml(style.label)}
|
|
2318
|
+
<span class="gfp-check-count">${kg.edges.filter(e => e.edgeType === type).length}</span>
|
|
2319
|
+
</button>
|
|
2320
|
+
`).join('')}
|
|
2321
|
+
</div>
|
|
2322
|
+
</div>
|
|
2323
|
+
`;
|
|
2324
|
+
|
|
2325
|
+
const searchHtml = `
|
|
2326
|
+
<div class="gfp-section">
|
|
2327
|
+
<div class="gfp-label">${t('graphSearch')}</div>
|
|
2328
|
+
<input type="text" class="gfp-search" id="kg-search" placeholder="${t('graphFindEntity')}" autocomplete="off" />
|
|
2329
|
+
</div>
|
|
2330
|
+
`;
|
|
2331
|
+
|
|
2332
|
+
const viewModeHtml = `
|
|
2333
|
+
<div class="gfp-section">
|
|
2334
|
+
<div class="gfp-label">${t('kgViewMode')}</div>
|
|
2335
|
+
<div class="gfp-depth-row">
|
|
2336
|
+
<button class="gfp-depth-btn${focusedMode ? ' active' : ''}" data-view-mode="focused">${t('kgFocused')}</button>
|
|
2337
|
+
<button class="gfp-depth-btn${!focusedMode ? ' active' : ''}" data-view-mode="full">${t('kgFullGraph')}</button>
|
|
2338
|
+
</div>
|
|
2339
|
+
</div>
|
|
2340
|
+
`;
|
|
2341
|
+
|
|
2342
|
+
panel.innerHTML = searchHtml + viewModeHtml + clusterHtml + edgeTypeHtml;
|
|
2343
|
+
|
|
2344
|
+
// Bind section filters
|
|
2345
|
+
panel.querySelectorAll('[data-section-filter]').forEach(btn => {
|
|
2346
|
+
btn.addEventListener('click', () => {
|
|
2347
|
+
const id = btn.dataset.sectionFilter;
|
|
2348
|
+
if (activeSections.has(id)) activeSections.delete(id);
|
|
2349
|
+
else activeSections.add(id);
|
|
2350
|
+
initEChartsGraph();
|
|
2351
|
+
renderKGFilterPanel();
|
|
2352
|
+
});
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
// Bind edge type filters
|
|
2356
|
+
panel.querySelectorAll('[data-edge-type-filter]').forEach(btn => {
|
|
2357
|
+
btn.addEventListener('click', () => {
|
|
2358
|
+
const type = btn.dataset.edgeTypeFilter;
|
|
2359
|
+
if (activeEdgeTypes.has(type)) activeEdgeTypes.delete(type);
|
|
2360
|
+
else activeEdgeTypes.add(type);
|
|
2361
|
+
initEChartsGraph();
|
|
2362
|
+
renderKGFilterPanel();
|
|
2363
|
+
});
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
// Bind view mode toggle
|
|
2367
|
+
panel.querySelectorAll('[data-view-mode]').forEach(btn => {
|
|
2368
|
+
btn.addEventListener('click', () => {
|
|
2369
|
+
const mode = btn.dataset.viewMode;
|
|
2370
|
+
focusedMode = mode === 'focused';
|
|
2371
|
+
initEChartsGraph();
|
|
2372
|
+
renderKGFilterPanel();
|
|
2373
|
+
});
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
// Bind search
|
|
2377
|
+
const searchInput = document.getElementById('kg-search');
|
|
2378
|
+
if (searchInput) {
|
|
2379
|
+
searchInput.addEventListener('input', () => {
|
|
2380
|
+
const q = searchInput.value.toLowerCase();
|
|
2381
|
+
if (!echartsInstance) return;
|
|
2382
|
+
try {
|
|
2383
|
+
echartsInstance.dispatchAction({ type: 'downplay', seriesIndex: 0 });
|
|
2384
|
+
} catch (_) { /* noop */ }
|
|
2385
|
+
if (!q) return;
|
|
2386
|
+
// Highlight matched nodes (which auto-blurs others via emphasis.focus 'adjacency' isn't ideal here;
|
|
2387
|
+
// instead, use 'highlight' on matching dataIndex array for visual emphasis)
|
|
2388
|
+
const data = buildEChartsData();
|
|
2389
|
+
const matchIndexes = [];
|
|
2390
|
+
data.nodes.forEach((n, idx) => {
|
|
2391
|
+
const match = (n.fullLabel || '').toLowerCase().includes(q) ||
|
|
2392
|
+
(n.nodeType || '').toLowerCase().includes(q) ||
|
|
2393
|
+
(n.entityName || '').toLowerCase().includes(q);
|
|
2394
|
+
if (match) matchIndexes.push(idx);
|
|
2395
|
+
});
|
|
2396
|
+
if (matchIndexes.length > 0) {
|
|
2397
|
+
try {
|
|
2398
|
+
echartsInstance.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: matchIndexes });
|
|
2399
|
+
} catch (_) { /* noop */ }
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
initEChartsGraph();
|
|
2406
|
+
renderKGFilterPanel();
|
|
2407
|
+
}
|
|
2408
|
+
|
|
1485
2409
|
// ============================================================
|
|
1486
2410
|
// Cytoscape.js + Dagre — Focused Topology Renderer
|
|
1487
2411
|
// Default: 1-hop neighborhood of top entity, dagre LR layout
|
|
@@ -2340,7 +3264,7 @@ async function loadObservations() {
|
|
|
2340
3264
|
</div>
|
|
2341
3265
|
<div style="display:flex;gap:8px;">
|
|
2342
3266
|
<button class="export-btn" id="btn-batch-cleanup" title="${t('batchCleanup')}">
|
|
2343
|
-
|
|
3267
|
+
[CLEANUP] ${t('batchCleanup')}
|
|
2344
3268
|
</button>
|
|
2345
3269
|
<button class="export-btn" id="btn-export" title="${t('exportData')}">
|
|
2346
3270
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M4 7l4 4 4-4M2 12v2h12v-2"/></svg>
|
|
@@ -2439,16 +3363,16 @@ function renderObsList() {
|
|
|
2439
3363
|
${batchMode ? `<input type="checkbox" class="obs-checkbox" ${isSelected ? 'checked' : ''} onclick="event.stopPropagation(); toggleObsSelect(${obs.id});" />` : ''}
|
|
2440
3364
|
<span class="obs-card-id">#${obs.id}</span>
|
|
2441
3365
|
<span class="type-badge" data-type="${obs.type || 'unknown'}">
|
|
2442
|
-
${typeIcons[obs.type] || '
|
|
3366
|
+
${typeIcons[obs.type] || '[UNKNOWN]'} ${obs.type || t('unknown')}
|
|
2443
3367
|
</span>
|
|
2444
3368
|
${isLow ? '<span class="low-quality-badge">' + t('lowQuality') + '</span>' : ''}
|
|
2445
3369
|
<span class="obs-card-title">${hl(obs.title || t('untitled'))}</span>
|
|
2446
3370
|
<span class="obs-expand-icon">▼</span>
|
|
2447
3371
|
</div>
|
|
2448
3372
|
<div class="obs-card-meta">
|
|
2449
|
-
<span
|
|
2450
|
-
${obs.createdAt ? `<span
|
|
2451
|
-
${obs.accessCount ? `<span
|
|
3373
|
+
<span>[FILES] ${hl(obs.entityName || t('unknown'))}</span>
|
|
3374
|
+
${obs.createdAt ? `<span>[TIME] ${formatTime(obs.createdAt)}</span>` : ''}
|
|
3375
|
+
${obs.accessCount ? `<span>[VIEW] ${obs.accessCount}</span>` : ''}
|
|
2452
3376
|
</div>
|
|
2453
3377
|
<div class="obs-detail" id="obs-detail-${obs.id}" style="display:none;">
|
|
2454
3378
|
<div class="obs-detail-inner">
|
|
@@ -2479,7 +3403,7 @@ async function loadRetention() {
|
|
|
2479
3403
|
|
|
2480
3404
|
const data = await api('retention');
|
|
2481
3405
|
if (!data || data.items.length === 0) {
|
|
2482
|
-
container.innerHTML = emptyState('
|
|
3406
|
+
container.innerHTML = emptyState('[RETENTION]', t('noRetentionData'), t('noRetentionDesc'));
|
|
2483
3407
|
return;
|
|
2484
3408
|
}
|
|
2485
3409
|
|
|
@@ -2647,7 +3571,7 @@ async function loadGitMemory() {
|
|
|
2647
3571
|
|
|
2648
3572
|
const [stats, allObs] = await Promise.all([api('stats'), api('observations')]);
|
|
2649
3573
|
if (!stats || !allObs) {
|
|
2650
|
-
container.innerHTML = emptyState('
|
|
3574
|
+
container.innerHTML = emptyState('[MERGE]', t('noGitMemory'), t('noGitMemoryDesc'));
|
|
2651
3575
|
return;
|
|
2652
3576
|
}
|
|
2653
3577
|
|
|
@@ -2689,7 +3613,7 @@ async function loadGitMemory() {
|
|
|
2689
3613
|
${gitObs.length === 0 ? `
|
|
2690
3614
|
<div class="panel">
|
|
2691
3615
|
<div class="panel-body" style="text-align:center;padding:48px;">
|
|
2692
|
-
<div style="font-size:36px;margin-bottom:12px;"
|
|
3616
|
+
<div style="font-size:36px;margin-bottom:12px;">[MERGE]</div>
|
|
2693
3617
|
<div style="font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">${t('noGitMemoriesYet')}</div>
|
|
2694
3618
|
<div style="font-size:13px;color:var(--text-muted);max-width:400px;margin:0 auto;">
|
|
2695
3619
|
${t('noGitMemoriesHint')}<br>
|
|
@@ -3184,7 +4108,7 @@ async function loadTeam() {
|
|
|
3184
4108
|
<div style="font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">${t('teamNoData')}</div>
|
|
3185
4109
|
<div style="font-size:13px;color:var(--text-muted);max-width:480px;margin:0 auto;line-height:1.6;">
|
|
3186
4110
|
${t('teamNoDataHint')}<br>
|
|
3187
|
-
<code style="background:var(--bg-surface);padding:4px 10px;border-radius:6px;margin-top:8px;display:inline-block;font-size:12px;">memorix
|
|
4111
|
+
<code style="background:var(--bg-surface);padding:4px 10px;border-radius:6px;margin-top:8px;display:inline-block;font-size:12px;">memorix orchestrate · memorix team status · memorix task list</code>
|
|
3188
4112
|
</div>
|
|
3189
4113
|
</div>
|
|
3190
4114
|
</div>
|