@jungjaehoon/mama-os 0.61.2 → 0.63.0
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 +106 -0
- package/README.md +1 -1
- package/dist/api/drive-actions.d.ts +12 -0
- package/dist/api/drive-actions.d.ts.map +1 -0
- package/dist/api/drive-actions.js +322 -0
- package/dist/api/drive-actions.js.map +1 -0
- package/dist/api/report-source-actions.d.ts.map +1 -1
- package/dist/api/report-source-actions.js +15 -7
- package/dist/api/report-source-actions.js.map +1 -1
- package/dist/api/security-events.d.ts +26 -0
- package/dist/api/security-events.d.ts.map +1 -1
- package/dist/api/security-events.js +107 -24
- package/dist/api/security-events.js.map +1 -1
- package/dist/api/source-actions.d.ts.map +1 -1
- package/dist/api/source-actions.js +7 -7
- package/dist/api/source-actions.js.map +1 -1
- package/dist/api/stored-source-reader.d.ts.map +1 -1
- package/dist/api/stored-source-reader.js +3 -0
- package/dist/api/stored-source-reader.js.map +1 -1
- package/dist/api/trello-actions.d.ts +15 -0
- package/dist/api/trello-actions.d.ts.map +1 -0
- package/dist/api/trello-actions.js +130 -0
- package/dist/api/trello-actions.js.map +1 -0
- package/dist/api/viewer-data.d.ts +77 -0
- package/dist/api/viewer-data.d.ts.map +1 -1
- package/dist/api/viewer-data.js +116 -0
- package/dist/api/viewer-data.js.map +1 -1
- package/dist/api/viewer-server.d.ts +1 -1
- package/dist/api/viewer-server.d.ts.map +1 -1
- package/dist/api/viewer-server.js +87 -3
- package/dist/api/viewer-server.js.map +1 -1
- package/dist/api/work-actions.d.ts +3 -0
- package/dist/api/work-actions.d.ts.map +1 -1
- package/dist/api/work-actions.js +23 -15
- package/dist/api/work-actions.js.map +1 -1
- package/dist/backfill/format.d.ts +105 -0
- package/dist/backfill/format.d.ts.map +1 -0
- package/dist/backfill/format.js +314 -0
- package/dist/backfill/format.js.map +1 -0
- package/dist/backfill/push.d.ts +63 -0
- package/dist/backfill/push.d.ts.map +1 -0
- package/dist/backfill/push.js +221 -0
- package/dist/backfill/push.js.map +1 -0
- package/dist/cli/commands/backfill.d.ts +12 -0
- package/dist/cli/commands/backfill.d.ts.map +1 -0
- package/dist/cli/commands/backfill.js +67 -0
- package/dist/cli/commands/backfill.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts +2 -1
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +55 -14
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +4 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/index.js +18 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/runtime/claude-caller-config.d.ts +6 -1
- package/dist/cli/runtime/claude-caller-config.d.ts.map +1 -1
- package/dist/cli/runtime/claude-caller-config.js +3 -1
- package/dist/cli/runtime/claude-caller-config.js.map +1 -1
- package/dist/connectors/framework/gws-utils.d.ts +6 -3
- package/dist/connectors/framework/gws-utils.d.ts.map +1 -1
- package/dist/connectors/framework/gws-utils.js +35 -6
- package/dist/connectors/framework/gws-utils.js.map +1 -1
- package/dist/connectors/index.d.ts +0 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js +1 -5
- package/dist/connectors/index.js.map +1 -1
- package/dist/connectors/slack/index.d.ts +2 -0
- package/dist/connectors/slack/index.d.ts.map +1 -1
- package/dist/connectors/slack/index.js +83 -39
- package/dist/connectors/slack/index.js.map +1 -1
- package/dist/connectors/trello/action-line.d.ts +13 -0
- package/dist/connectors/trello/action-line.d.ts.map +1 -0
- package/dist/connectors/trello/action-line.js +118 -0
- package/dist/connectors/trello/action-line.js.map +1 -0
- package/dist/connectors/trello/actions.d.ts +27 -0
- package/dist/connectors/trello/actions.d.ts.map +1 -0
- package/dist/connectors/trello/actions.js +35 -0
- package/dist/connectors/trello/actions.js.map +1 -0
- package/dist/connectors/trello/api.d.ts +23 -0
- package/dist/connectors/trello/api.d.ts.map +1 -0
- package/dist/connectors/trello/api.js +102 -0
- package/dist/connectors/trello/api.js.map +1 -0
- package/dist/connectors/trello/index.d.ts +63 -31
- package/dist/connectors/trello/index.d.ts.map +1 -1
- package/dist/connectors/trello/index.js +165 -246
- package/dist/connectors/trello/index.js.map +1 -1
- package/dist/gateways/discord.d.ts +4 -0
- package/dist/gateways/discord.d.ts.map +1 -1
- package/dist/gateways/discord.js +7 -3
- package/dist/gateways/discord.js.map +1 -1
- package/dist/gateways/slack.d.ts +4 -0
- package/dist/gateways/slack.d.ts.map +1 -1
- package/dist/gateways/slack.js +7 -3
- package/dist/gateways/slack.js.map +1 -1
- package/dist/gateways/telegram-message-ledger.d.ts +5 -0
- package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
- package/dist/gateways/telegram-message-ledger.js +8 -1
- package/dist/gateways/telegram-message-ledger.js.map +1 -1
- package/dist/gateways/telegram.d.ts +4 -0
- package/dist/gateways/telegram.d.ts.map +1 -1
- package/dist/gateways/telegram.js +7 -5
- package/dist/gateways/telegram.js.map +1 -1
- package/dist/replay/replay-source-catalog.d.ts.map +1 -1
- package/dist/replay/replay-source-catalog.js +5 -45
- package/dist/replay/replay-source-catalog.js.map +1 -1
- package/dist/replay/trello-import.d.ts +1 -8
- package/dist/replay/trello-import.d.ts.map +1 -1
- package/dist/replay/trello-import.js +16 -103
- package/dist/replay/trello-import.js.map +1 -1
- package/dist/runtime/action-surface.d.ts +4 -1
- package/dist/runtime/action-surface.d.ts.map +1 -1
- package/dist/runtime/action-surface.js +30 -1
- package/dist/runtime/action-surface.js.map +1 -1
- package/dist/runtime/config.d.ts +5 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +10 -1
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/connectors.d.ts +0 -1
- package/dist/runtime/connectors.d.ts.map +1 -1
- package/dist/runtime/connectors.js +0 -1
- package/dist/runtime/connectors.js.map +1 -1
- package/dist/runtime/egress-proxy.d.ts +15 -0
- package/dist/runtime/egress-proxy.d.ts.map +1 -0
- package/dist/runtime/egress-proxy.js +176 -0
- package/dist/runtime/egress-proxy.js.map +1 -0
- package/dist/runtime/native-session.d.ts +7 -0
- package/dist/runtime/native-session.d.ts.map +1 -1
- package/dist/runtime/native-session.js +8 -1
- package/dist/runtime/native-session.js.map +1 -1
- package/dist/runtime/outbound-attempts.d.ts +18 -0
- package/dist/runtime/outbound-attempts.d.ts.map +1 -0
- package/dist/runtime/outbound-attempts.js +98 -0
- package/dist/runtime/outbound-attempts.js.map +1 -0
- package/dist/runtime/owner-runtime.d.ts +9 -0
- package/dist/runtime/owner-runtime.d.ts.map +1 -1
- package/dist/runtime/owner-runtime.js +8 -0
- package/dist/runtime/owner-runtime.js.map +1 -1
- package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
- package/dist/runtime/owner-system-prompt.js +6 -1
- package/dist/runtime/owner-system-prompt.js.map +1 -1
- package/dist/runtime/record-orders.d.ts.map +1 -1
- package/dist/runtime/record-orders.js +33 -13
- package/dist/runtime/record-orders.js.map +1 -1
- package/dist/runtime/stimulus-delivery.d.ts +15 -0
- package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
- package/dist/runtime/stimulus-delivery.js +29 -2
- package/dist/runtime/stimulus-delivery.js.map +1 -1
- package/dist/runtime/turn-orders.d.ts.map +1 -1
- package/dist/runtime/turn-orders.js +6 -2
- package/dist/runtime/turn-orders.js.map +1 -1
- package/package.json +2 -2
- package/public/viewer/js/modules/graph.js +169 -957
- package/public/viewer/js/modules/memory-timeline.js +215 -0
- package/public/viewer/js/utils/api.js +7 -26
- package/public/viewer/log-viewer.html +7 -3
- package/public/viewer/operator/operator.css +1 -1
- package/public/viewer/operator/operator.js +5428 -5685
- package/public/viewer/src/modules/graph.ts +185 -1132
- package/public/viewer/src/modules/memory-timeline.ts +272 -0
- package/public/viewer/src/types/global.d.ts +1 -1
- package/public/viewer/src/utils/api.ts +43 -47
- package/public/viewer/viewer.css +257 -7
- package/public/viewer/viewer.html +68 -292
|
@@ -1,47 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Graph Module -
|
|
2
|
+
* Graph Module - one memory record and what it links to
|
|
3
3
|
* @module modules/graph
|
|
4
|
-
* @version 1.0.0
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - Search and filter functionality
|
|
10
|
-
* - Detail panel for node information
|
|
11
|
-
* - BFS traversal for connected nodes
|
|
5
|
+
* The memory view picks a record from the saved timeline. This draws that record with its direct
|
|
6
|
+
* links (the source messages it rests on, what it builds on, what it replaced), names every colour
|
|
7
|
+
* and line in plain words, and shows the detail of whichever dot is clicked.
|
|
12
8
|
*/
|
|
13
9
|
/* eslint-env browser */
|
|
14
10
|
/* global vis */
|
|
15
|
-
import { escapeHtml,
|
|
11
|
+
import { escapeHtml, getElementByIdOrNull } from '../utils/dom.js';
|
|
16
12
|
import { DebugLogger } from '../utils/debug-logger.js';
|
|
17
|
-
import { API
|
|
13
|
+
import { API } from '../utils/api.js';
|
|
18
14
|
import { renderSafeMarkdown } from '../utils/markdown.js';
|
|
19
15
|
const logger = new DebugLogger('Graph');
|
|
20
|
-
|
|
16
|
+
/** What each dot colour stands for, in the words the legend shows. */
|
|
17
|
+
export const KIND_LABELS = {
|
|
18
|
+
decision: 'Decision',
|
|
19
|
+
preference: 'Preference',
|
|
20
|
+
constraint: 'Constraint',
|
|
21
|
+
lesson: 'Lesson',
|
|
22
|
+
workflow: 'Workflow',
|
|
23
|
+
fact: 'Fact',
|
|
24
|
+
commitment: 'Work update',
|
|
25
|
+
observation: 'Source message',
|
|
26
|
+
memory: 'Memory without a kind',
|
|
27
|
+
registry: 'Registered name',
|
|
28
|
+
};
|
|
29
|
+
/** Every relation the engine stores, read from the record that holds the link. */
|
|
30
|
+
export const RELATION_LABELS = {
|
|
31
|
+
derived_from: 'rests on (evidence)',
|
|
32
|
+
mentions: 'mentions',
|
|
33
|
+
builds_on: 'builds on',
|
|
34
|
+
amends: 'amends',
|
|
35
|
+
refines: 'refines',
|
|
36
|
+
supersedes: 'replaces',
|
|
37
|
+
contradicts: 'contradicts',
|
|
38
|
+
debates: 'argues with',
|
|
39
|
+
synthesizes: 'combines',
|
|
40
|
+
case_member: 'belongs to case',
|
|
41
|
+
alias_of: 'another name for',
|
|
42
|
+
next_action_for: 'next step for',
|
|
43
|
+
blocks: 'blocks',
|
|
44
|
+
};
|
|
45
|
+
const LABEL_LENGTH = 40;
|
|
46
|
+
function shortLabel(text) {
|
|
47
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
48
|
+
return flat.length > LABEL_LENGTH ? `${flat.slice(0, LABEL_LENGTH - 1)}…` : flat;
|
|
49
|
+
}
|
|
21
50
|
/**
|
|
22
51
|
* Graph Module Class
|
|
23
52
|
*/
|
|
24
53
|
export class GraphModule {
|
|
25
54
|
network = null;
|
|
26
55
|
graphData = { nodes: [], edges: [], meta: {} };
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
debouncedSearch = debounce(() => this.search(), 300);
|
|
32
|
-
similarDecisionClickHandler = (event) => {
|
|
33
|
-
const target = event.target;
|
|
34
|
-
const btn = target.closest('.similar-decision-btn');
|
|
35
|
-
if (!btn || !btn.dataset.nodeId) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
this.navigateToNode(btn.dataset.nodeId);
|
|
39
|
-
};
|
|
56
|
+
centerId = null;
|
|
57
|
+
// A slower earlier read must not draw or describe a record the owner has moved away from.
|
|
58
|
+
recordRequest = 0;
|
|
59
|
+
detailRequest = 0;
|
|
40
60
|
nodeColors = {
|
|
41
61
|
decision: '#60A5FA',
|
|
42
62
|
preference: '#C084FC',
|
|
43
63
|
constraint: '#FB7185',
|
|
44
64
|
lesson: '#FBBF24',
|
|
65
|
+
workflow: '#F472B6',
|
|
45
66
|
fact: '#2DD4BF',
|
|
46
67
|
commitment: '#FB923C',
|
|
47
68
|
observation: '#94A3B8',
|
|
@@ -54,35 +75,39 @@ export class GraphModule {
|
|
|
54
75
|
derived_from: { color: '#3A9E7E', dashes: false, width: 2 },
|
|
55
76
|
amends: { color: '#2563EB', dashes: [8, 4], width: 2 },
|
|
56
77
|
refines: { color: '#B8860B', dashes: false, width: 2 },
|
|
57
|
-
builds_on: { color: '#B8860B', dashes: [5, 5], width: 2.5 },
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
builds_on: { color: '#B8860B', dashes: [5, 5], width: 2.5 },
|
|
79
|
+
contradicts: { color: '#DC143C', dashes: false, width: 2.5 },
|
|
80
|
+
debates: { color: '#DC143C', dashes: [5, 5], width: 2.5 },
|
|
81
|
+
synthesizes: { color: '#6B4C9A', width: 3, dashes: false },
|
|
60
82
|
};
|
|
61
|
-
constructor() {
|
|
62
|
-
// Network state
|
|
63
|
-
}
|
|
64
|
-
// =============================================
|
|
65
|
-
// Data Loading
|
|
66
|
-
// =============================================
|
|
67
83
|
/**
|
|
68
|
-
*
|
|
84
|
+
* Draw a record and its direct links, then show its detail.
|
|
69
85
|
*/
|
|
70
|
-
async
|
|
86
|
+
async showRecord(id) {
|
|
87
|
+
const request = ++this.recordRequest;
|
|
88
|
+
const empty = getElementByIdOrNull('graph-empty');
|
|
89
|
+
const loading = getElementByIdOrNull('graph-loading');
|
|
90
|
+
if (empty)
|
|
91
|
+
empty.style.display = 'none';
|
|
92
|
+
if (loading)
|
|
93
|
+
loading.style.display = 'flex';
|
|
71
94
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
const data = (await API.getGraphNeighbors(id));
|
|
96
|
+
if (request !== this.recordRequest)
|
|
97
|
+
return;
|
|
98
|
+
this.centerId = id;
|
|
99
|
+
this.init(data);
|
|
100
|
+
const center = data.nodes.find((node) => String(node.id) === id);
|
|
101
|
+
if (center)
|
|
102
|
+
void this.showDetail(center);
|
|
75
103
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
finally {
|
|
105
|
+
if (loading && request === this.recordRequest)
|
|
106
|
+
loading.style.display = 'none';
|
|
79
107
|
}
|
|
80
108
|
}
|
|
81
|
-
// =============================================
|
|
82
|
-
// Graph Initialization
|
|
83
|
-
// =============================================
|
|
84
109
|
/**
|
|
85
|
-
* Initialize vis-network
|
|
110
|
+
* Initialize vis-network over one record's neighbourhood.
|
|
86
111
|
*/
|
|
87
112
|
init(data) {
|
|
88
113
|
const container = getElementByIdOrNull('graph-canvas');
|
|
@@ -90,33 +115,30 @@ export class GraphModule {
|
|
|
90
115
|
logger.error('graph-canvas element not found');
|
|
91
116
|
return;
|
|
92
117
|
}
|
|
93
|
-
// Ensure container has dimensions for vis-network
|
|
94
|
-
if (container.offsetHeight === 0) {
|
|
95
|
-
container.style.minHeight = '400px';
|
|
96
|
-
}
|
|
97
|
-
logger.debug('Graph canvas dimensions:', container.offsetWidth, 'x', container.offsetHeight);
|
|
98
118
|
this.graphData = data;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
this.network?.destroy?.();
|
|
120
|
+
const nodes = data.nodes.map((n) => {
|
|
121
|
+
const kind = n.kind ?? 'memory';
|
|
122
|
+
const center = String(n.id) === this.centerId;
|
|
123
|
+
const color = this.getNodeColor(kind);
|
|
124
|
+
return {
|
|
125
|
+
id: n.id,
|
|
126
|
+
// Source messages stay unlabelled dots: their time and text are on hover and click.
|
|
127
|
+
label: kind === 'observation' && !center
|
|
128
|
+
? undefined
|
|
129
|
+
: shortLabel(String(n.decision_preview ?? n.decision ?? n.topic ?? n.id)),
|
|
130
|
+
title: this.createNodeTooltip(n),
|
|
131
|
+
color: {
|
|
132
|
+
background: color,
|
|
133
|
+
border: center ? '#131313' : color,
|
|
134
|
+
highlight: { background: color, border: '#131313' },
|
|
135
|
+
},
|
|
136
|
+
size: center ? 22 : kind === 'observation' ? 9 : 14,
|
|
137
|
+
borderWidth: center ? 3 : 1,
|
|
138
|
+
font: { color: '#131313', size: center ? 15 : 13 },
|
|
139
|
+
data: n,
|
|
140
|
+
};
|
|
141
|
+
});
|
|
120
142
|
const edges = data.edges.map((e) => {
|
|
121
143
|
const style = this.getEdgeStyle(e.relationship);
|
|
122
144
|
return {
|
|
@@ -125,180 +147,95 @@ export class GraphModule {
|
|
|
125
147
|
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
|
126
148
|
color: style.color,
|
|
127
149
|
dashes: style.dashes,
|
|
128
|
-
width: style.width
|
|
129
|
-
title: e.relationship,
|
|
150
|
+
width: style.width,
|
|
151
|
+
title: this.relationLabel(e.relationship),
|
|
130
152
|
};
|
|
131
153
|
});
|
|
132
|
-
|
|
133
|
-
nodes:
|
|
134
|
-
edges:
|
|
135
|
-
};
|
|
136
|
-
const options = {
|
|
137
|
-
nodes: {
|
|
138
|
-
shape: 'dot',
|
|
139
|
-
scaling: {
|
|
140
|
-
min: 10,
|
|
141
|
-
max: 30,
|
|
142
|
-
label: { enabled: true, min: 12, max: 22, maxVisible: 22, drawThreshold: 8 },
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
edges: {
|
|
146
|
-
smooth: { type: 'continuous', roundness: 0.5 },
|
|
147
|
-
width: 2,
|
|
148
|
-
},
|
|
154
|
+
this.network = new vis.Network(container, { nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges) }, {
|
|
155
|
+
nodes: { shape: 'dot' },
|
|
156
|
+
edges: { smooth: { type: 'continuous', roundness: 0.5 }, width: 2 },
|
|
149
157
|
physics: {
|
|
150
|
-
// The initial layout always runs; the stabilized handler turns physics off afterwards.
|
|
151
158
|
enabled: true,
|
|
152
159
|
barnesHut: {
|
|
153
|
-
gravitationalConstant: -
|
|
154
|
-
centralGravity: 0.
|
|
155
|
-
springLength:
|
|
160
|
+
gravitationalConstant: -6000,
|
|
161
|
+
centralGravity: 0.4,
|
|
162
|
+
springLength: 140,
|
|
156
163
|
springConstant: 0.04,
|
|
157
164
|
damping: 0.09,
|
|
158
165
|
avoidOverlap: 1,
|
|
159
166
|
},
|
|
160
|
-
stabilization: {
|
|
161
|
-
enabled: true,
|
|
162
|
-
iterations: data.nodes.length > 500 ? 100 : 200,
|
|
163
|
-
updateInterval: 25,
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
interaction: {
|
|
167
|
-
hover: true,
|
|
168
|
-
tooltipDelay: 100,
|
|
169
|
-
zoomView: true,
|
|
170
|
-
dragView: true,
|
|
167
|
+
stabilization: { enabled: true, iterations: 200, updateInterval: 25 },
|
|
171
168
|
},
|
|
172
|
-
|
|
173
|
-
this.network = new vis.Network(container, networkData, options);
|
|
174
|
-
// Event handlers
|
|
175
|
-
this.network.on('click', (params) => {
|
|
176
|
-
try {
|
|
177
|
-
if (params.nodes.length > 0) {
|
|
178
|
-
const nodeId = params.nodes[0];
|
|
179
|
-
const targetId = String(nodeId);
|
|
180
|
-
const node = data.nodes.find((n) => String(n.id) === targetId);
|
|
181
|
-
if (node) {
|
|
182
|
-
logger.debug('Node clicked:', nodeId, node);
|
|
183
|
-
this.showDetail(node);
|
|
184
|
-
this.highlightConnectedNodes(targetId);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
this.closeDetail();
|
|
189
|
-
this.resetNodeHighlight();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
logger.error('Error handling click:', error);
|
|
194
|
-
if (error instanceof Error && error.stack) {
|
|
195
|
-
logger.error('Error stack:', error.stack);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
169
|
+
interaction: { hover: true, tooltipDelay: 100, zoomView: true, dragView: true },
|
|
198
170
|
});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
id: params.node,
|
|
207
|
-
font: { color: '#131313', size: Math.max(fontSize, 14), vadjust: -2 },
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
/* ignore */
|
|
212
|
-
}
|
|
171
|
+
this.network.on('click', (params) => {
|
|
172
|
+
const targetId = params.nodes[0];
|
|
173
|
+
if (targetId === undefined)
|
|
174
|
+
return;
|
|
175
|
+
const node = data.nodes.find((n) => String(n.id) === String(targetId));
|
|
176
|
+
if (node)
|
|
177
|
+
void this.showDetail(node);
|
|
213
178
|
});
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
/* ignore */
|
|
179
|
+
// Stepping to a linked record redraws the graph around it.
|
|
180
|
+
this.network.on('doubleClick', (params) => {
|
|
181
|
+
const targetId = params.nodes[0];
|
|
182
|
+
if (targetId !== undefined && String(targetId) !== this.centerId) {
|
|
183
|
+
void this.showRecord(String(targetId));
|
|
223
184
|
}
|
|
224
185
|
});
|
|
225
|
-
// A large graph may never reach vis-network's 'stabilized'; stop when the bounded iterations end.
|
|
226
186
|
this.network.on('stabilizationIterationsDone', () => {
|
|
227
187
|
this.network?.setOptions?.({ physics: { enabled: false } });
|
|
228
188
|
});
|
|
229
|
-
this.network.on('stabilized', () => {
|
|
230
|
-
const loadingEl = getElementByIdOrNull('graph-loading');
|
|
231
|
-
if (loadingEl) {
|
|
232
|
-
loadingEl.style.display = 'none';
|
|
233
|
-
}
|
|
234
|
-
// Disable physics after initial layout to free CPU
|
|
235
|
-
this.network?.setOptions?.({ physics: { enabled: false } });
|
|
236
|
-
logger.info('Graph stabilized, physics disabled');
|
|
237
|
-
});
|
|
238
|
-
// Backup: hide loading after 3 seconds even if stabilization doesn't complete
|
|
239
|
-
setTimeout(() => {
|
|
240
|
-
const loadingEl = getElementByIdOrNull('graph-loading');
|
|
241
|
-
if (loadingEl && loadingEl.style.display !== 'none') {
|
|
242
|
-
loadingEl.style.display = 'none';
|
|
243
|
-
logger.warn('Graph loading hidden by timeout');
|
|
244
|
-
}
|
|
245
|
-
}, 3000);
|
|
246
|
-
// Populate topic filter — sorted by most recent created_at (newest first)
|
|
247
|
-
const topicLatest = new Map();
|
|
248
|
-
for (const n of data.nodes) {
|
|
249
|
-
const t = n.topic || '';
|
|
250
|
-
const ts = typeof n.created_at === 'number' ? n.created_at : 0;
|
|
251
|
-
if (!topicLatest.has(t) || ts > topicLatest.get(t)) {
|
|
252
|
-
topicLatest.set(t, ts);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
const topics = [...topicLatest.entries()].sort((a, b) => b[1] - a[1]).map(([topic]) => topic);
|
|
256
|
-
this.populateTopicFilter(topics);
|
|
257
|
-
this.populateDecisionList(data.nodes);
|
|
258
189
|
this.renderLegend();
|
|
190
|
+
const legend = getElementByIdOrNull('legend-panel');
|
|
191
|
+
legend?.classList.add('visible');
|
|
259
192
|
const stats = getElementByIdOrNull('graph-stats');
|
|
260
193
|
if (stats) {
|
|
261
|
-
stats.
|
|
194
|
+
stats.style.display = '';
|
|
195
|
+
stats.textContent = `${data.nodes.length} records and source messages · ${data.edges.length} links · click a dot for its detail, double-click to step to it`;
|
|
262
196
|
}
|
|
263
|
-
logger.info('Graph initialized with', nodes.length, 'nodes and', edges.length, 'edges');
|
|
264
197
|
}
|
|
265
198
|
// =============================================
|
|
266
|
-
// Styling
|
|
199
|
+
// Styling and legend
|
|
267
200
|
// =============================================
|
|
268
201
|
getNodeColor(kind = 'memory') {
|
|
269
202
|
return this.nodeColors[kind] ?? '#CBD5E1';
|
|
270
203
|
}
|
|
271
|
-
|
|
204
|
+
getEdgeStyle(relationship) {
|
|
205
|
+
return (this.edgeStyles[relationship ?? 'default'] ?? { color: '#4a4a6a', dashes: false, width: 2 });
|
|
206
|
+
}
|
|
207
|
+
kindLabel(kind) {
|
|
208
|
+
return KIND_LABELS[kind] ?? kind;
|
|
209
|
+
}
|
|
210
|
+
relationLabel(relationship) {
|
|
211
|
+
return relationship === undefined ? 'link' : (RELATION_LABELS[relationship] ?? relationship);
|
|
212
|
+
}
|
|
213
|
+
/** Count what is drawn, by kind and by relationship, with the words the legend shows. */
|
|
272
214
|
getLegendEntries() {
|
|
273
|
-
const visible = new Set((this.network ? this.network.body.data.nodes.get() : this.graphData.nodes)
|
|
274
|
-
.filter((node) => !node.hidden)
|
|
275
|
-
.map((node) => String(node.id)));
|
|
276
215
|
const kinds = new Map();
|
|
277
216
|
const relations = new Map();
|
|
278
217
|
for (const node of this.graphData.nodes) {
|
|
279
|
-
if (!visible.has(String(node.id)))
|
|
280
|
-
continue;
|
|
281
218
|
const kind = node.kind ?? 'memory';
|
|
282
219
|
kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
|
|
283
220
|
}
|
|
284
221
|
for (const edge of this.graphData.edges) {
|
|
285
|
-
if (!visible.has(String(edge.from)) || !visible.has(String(edge.to)))
|
|
286
|
-
continue;
|
|
287
222
|
const relation = edge.relationship ?? 'default';
|
|
288
223
|
relations.set(relation, (relations.get(relation) ?? 0) + 1);
|
|
289
224
|
}
|
|
290
225
|
return {
|
|
291
226
|
nodes: [...kinds]
|
|
292
|
-
.sort(([a], [b]) => a
|
|
227
|
+
.sort(([, a], [, b]) => b - a)
|
|
293
228
|
.map(([kind, count]) => ({
|
|
294
229
|
kind,
|
|
230
|
+
label: this.kindLabel(kind),
|
|
295
231
|
count,
|
|
296
232
|
color: this.getNodeColor(kind),
|
|
297
233
|
})),
|
|
298
234
|
edges: [...relations]
|
|
299
|
-
.sort(([a], [b]) => a
|
|
235
|
+
.sort(([, a], [, b]) => b - a)
|
|
300
236
|
.map(([relationship, count]) => ({
|
|
301
237
|
relationship,
|
|
238
|
+
label: this.relationLabel(relationship),
|
|
302
239
|
count,
|
|
303
240
|
...this.getEdgeStyle(relationship),
|
|
304
241
|
})),
|
|
@@ -311,785 +248,60 @@ export class GraphModule {
|
|
|
311
248
|
const entries = this.getLegendEntries();
|
|
312
249
|
const section = (title, rows) => rows.length === 0
|
|
313
250
|
? ''
|
|
314
|
-
: `<div><div class="
|
|
315
|
-
const row = (sample, label, count) => `<div class="
|
|
251
|
+
: `<div><div class="legend-heading">${title}</div>${rows.join('')}</div>`;
|
|
252
|
+
const row = (sample, label, count) => `<div class="legend-row">${sample}<span class="legend-label">${escapeHtml(label)}</span><span class="legend-count">${count}</span></div>`;
|
|
316
253
|
container.innerHTML =
|
|
317
|
-
section('
|
|
318
|
-
section('
|
|
254
|
+
section('Dots', entries.nodes.map(({ label, count, color }) => row(`<span class="legend-dot" style="background:${color}"></span>`, label, count))) +
|
|
255
|
+
section('Lines (arrow points at what the record links to)', entries.edges.map(({ label, count, color, dashes, width }) => {
|
|
319
256
|
const dash = Array.isArray(dashes) ? dashes.join(' ') : dashes ? '5 5' : 'none';
|
|
320
|
-
return row(`<svg width="
|
|
257
|
+
return row(`<svg width="28" height="10" class="shrink-0" aria-hidden="true"><line x1="0" y1="5" x2="28" y2="5" stroke="${color}" stroke-width="${width}" stroke-dasharray="${dash}" /></svg>`, label, count);
|
|
321
258
|
}));
|
|
322
259
|
if (entries.nodes.length === 0)
|
|
323
|
-
container.textContent = '
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Get border color based on outcome
|
|
327
|
-
*/
|
|
328
|
-
getOutcomeBorderColor(outcome, fallback = '#4a4a6a') {
|
|
329
|
-
switch (outcome?.toLowerCase()) {
|
|
330
|
-
case 'success':
|
|
331
|
-
return '#22c55e';
|
|
332
|
-
case 'failed':
|
|
333
|
-
return '#ef4444';
|
|
334
|
-
case 'partial':
|
|
335
|
-
return '#f59e0b';
|
|
336
|
-
default:
|
|
337
|
-
return fallback;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Get edge style by relationship type
|
|
342
|
-
*/
|
|
343
|
-
getEdgeStyle(relationship) {
|
|
344
|
-
return (this.edgeStyles[relationship || 'default'] || { color: '#4a4a6a', dashes: false, width: 2 });
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Get node size based on connection count
|
|
348
|
-
*/
|
|
349
|
-
getNodeSize(connectionCount) {
|
|
350
|
-
if (connectionCount <= 2) {
|
|
351
|
-
return 12;
|
|
352
|
-
}
|
|
353
|
-
if (connectionCount <= 5) {
|
|
354
|
-
return 18;
|
|
355
|
-
}
|
|
356
|
-
if (connectionCount <= 10) {
|
|
357
|
-
return 24;
|
|
358
|
-
}
|
|
359
|
-
return 30;
|
|
260
|
+
container.textContent = 'Nothing drawn';
|
|
360
261
|
}
|
|
361
|
-
/**
|
|
362
|
-
* Create node tooltip
|
|
363
|
-
*/
|
|
364
262
|
createNodeTooltip(node) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
${node.outcome ? `Outcome: ${escapeHtml(node.outcome)}<br>` : ''}
|
|
369
|
-
Confidence: ${Math.round((node.confidence || 0) * 100)}%
|
|
370
|
-
`;
|
|
371
|
-
}
|
|
372
|
-
// =============================================
|
|
373
|
-
// Data Processing
|
|
374
|
-
// =============================================
|
|
375
|
-
/**
|
|
376
|
-
* Build adjacency list for BFS
|
|
377
|
-
*/
|
|
378
|
-
buildAdjacencyList(edges) {
|
|
379
|
-
this.adjacencyList = new Map();
|
|
380
|
-
edges.forEach((edge) => {
|
|
381
|
-
const from = String(edge.from);
|
|
382
|
-
const to = String(edge.to);
|
|
383
|
-
if (!this.adjacencyList.has(from)) {
|
|
384
|
-
this.adjacencyList.set(from, []);
|
|
385
|
-
}
|
|
386
|
-
if (!this.adjacencyList.has(to)) {
|
|
387
|
-
this.adjacencyList.set(to, []);
|
|
388
|
-
}
|
|
389
|
-
this.adjacencyList.get(from).push(to);
|
|
390
|
-
this.adjacencyList.get(to).push(from);
|
|
391
|
-
});
|
|
392
|
-
logger.debug('Adjacency list built with', this.adjacencyList.size, 'nodes');
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Calculate connection count for each node
|
|
396
|
-
*/
|
|
397
|
-
calculateConnectionCounts(nodes, edges) {
|
|
398
|
-
const counts = {};
|
|
399
|
-
nodes.forEach((n) => {
|
|
400
|
-
counts[String(n.id)] = 0;
|
|
401
|
-
});
|
|
402
|
-
edges.forEach((edge) => {
|
|
403
|
-
const from = String(edge.from);
|
|
404
|
-
const to = String(edge.to);
|
|
405
|
-
if (counts[from] !== undefined) {
|
|
406
|
-
counts[from]++;
|
|
407
|
-
}
|
|
408
|
-
if (counts[to] !== undefined) {
|
|
409
|
-
counts[to]++;
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
return counts;
|
|
413
|
-
}
|
|
414
|
-
// =============================================
|
|
415
|
-
// Graph Traversal & Highlighting
|
|
416
|
-
// =============================================
|
|
417
|
-
/**
|
|
418
|
-
* Get connected node IDs using BFS
|
|
419
|
-
*/
|
|
420
|
-
getConnectedNodeIds(nodeId, maxDepth = 3) {
|
|
421
|
-
const visited = new Set();
|
|
422
|
-
const queue = [{ id: nodeId, depth: 0 }];
|
|
423
|
-
visited.add(nodeId);
|
|
424
|
-
while (queue.length > 0) {
|
|
425
|
-
const next = queue.shift();
|
|
426
|
-
if (!next) {
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
const { id, depth } = next;
|
|
430
|
-
if (depth >= maxDepth) {
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
const neighbors = this.adjacencyList.get(id) || [];
|
|
434
|
-
neighbors.forEach((neighborId) => {
|
|
435
|
-
if (!visited.has(neighborId)) {
|
|
436
|
-
visited.add(neighborId);
|
|
437
|
-
queue.push({ id: neighborId, depth: depth + 1 });
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
return Array.from(visited);
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Highlight connected nodes
|
|
445
|
-
*/
|
|
446
|
-
highlightConnectedNodes(nodeId) {
|
|
447
|
-
const targetId = String(nodeId);
|
|
448
|
-
if (!this.network) {
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
const connectedIds = this.getConnectedNodeIds(targetId, 3);
|
|
452
|
-
const allNodes = this.network.body.data.nodes.get();
|
|
453
|
-
const nodeUpdates = allNodes.map((node) => {
|
|
454
|
-
const isConnected = connectedIds.includes(String(node.id));
|
|
455
|
-
const opacity = isConnected ? 1.0 : 0.2;
|
|
456
|
-
return {
|
|
457
|
-
id: node.id,
|
|
458
|
-
opacity: opacity,
|
|
459
|
-
font: { ...node.font, color: isConnected ? '#131313' : '#999' },
|
|
460
|
-
};
|
|
461
|
-
});
|
|
462
|
-
this.network.body.data.nodes.update(nodeUpdates);
|
|
463
|
-
const allEdges = this.network.body.data.edges.get();
|
|
464
|
-
const edgeUpdates = allEdges.map((edge) => {
|
|
465
|
-
const isConnected = connectedIds.includes(String(edge.from)) && connectedIds.includes(String(edge.to));
|
|
466
|
-
return {
|
|
467
|
-
id: edge.id,
|
|
468
|
-
opacity: isConnected ? 1.0 : 0.1,
|
|
469
|
-
};
|
|
470
|
-
});
|
|
471
|
-
this.network.body.data.edges.update(edgeUpdates);
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Reset node highlight
|
|
475
|
-
*/
|
|
476
|
-
resetNodeHighlight() {
|
|
477
|
-
if (!this.network) {
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const allNodes = this.network.body.data.nodes.get();
|
|
481
|
-
this.network.body.data.nodes.update(allNodes.map((node) => ({
|
|
482
|
-
id: node.id,
|
|
483
|
-
opacity: 1.0,
|
|
484
|
-
font: { ...node.font, color: '#131313' },
|
|
485
|
-
})));
|
|
486
|
-
const allEdges = this.network.body.data.edges.get();
|
|
487
|
-
this.network.body.data.edges.update(allEdges.map((edge) => ({
|
|
488
|
-
id: edge.id,
|
|
489
|
-
opacity: 1.0,
|
|
490
|
-
})));
|
|
263
|
+
const kind = this.kindLabel(node.kind ?? 'memory');
|
|
264
|
+
const text = String(node.decision_preview ?? node.decision ?? '');
|
|
265
|
+
return `${kind}${node.topic ? ` · ${node.topic}` : ''}\n${text.slice(0, 160)}`;
|
|
491
266
|
}
|
|
492
267
|
// =============================================
|
|
493
268
|
// Detail Panel
|
|
494
269
|
// =============================================
|
|
495
|
-
/**
|
|
496
|
-
* Show node detail panel
|
|
497
|
-
*/
|
|
498
270
|
async showDetail(node) {
|
|
499
|
-
try {
|
|
500
|
-
logger.debug('showDetail called with node:', node);
|
|
501
|
-
this.currentNodeId = String(node.id);
|
|
502
|
-
const panel = getElementByIdOrNull('decision-detail-modal');
|
|
503
|
-
if (!panel) {
|
|
504
|
-
logger.error('decision-detail-modal element not found');
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
// Update existing DOM elements with markdown rendering
|
|
508
|
-
const topicEl = getElementByIdOrNull('detail-topic');
|
|
509
|
-
const decisionEl = getElementByIdOrNull('detail-decision');
|
|
510
|
-
const reasoningEl = getElementByIdOrNull('detail-reasoning');
|
|
511
|
-
if (!decisionEl || !reasoningEl) {
|
|
512
|
-
logger.error('Required detail elements missing');
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
if (topicEl) {
|
|
516
|
-
topicEl.textContent = node.topic || 'Unknown Topic';
|
|
517
|
-
}
|
|
518
|
-
decisionEl.innerHTML = '<span class="loading-similar">Loading details...</span>';
|
|
519
|
-
reasoningEl.innerHTML = '<span class="loading-similar">Loading details...</span>';
|
|
520
|
-
const outcomeSelect = getElementByIdOrNull('detail-outcome-select');
|
|
521
|
-
if (outcomeSelect) {
|
|
522
|
-
outcomeSelect.value = node.outcome ? node.outcome.toUpperCase() : '';
|
|
523
|
-
outcomeSelect.disabled = Boolean((node.kind && node.kind !== 'memory') || (node.state && node.state !== 'current'));
|
|
524
|
-
}
|
|
525
|
-
// Clear outcome status
|
|
526
|
-
const outcomeStatus = getElementByIdOrNull('outcome-status');
|
|
527
|
-
if (outcomeStatus) {
|
|
528
|
-
outcomeStatus.textContent = '';
|
|
529
|
-
outcomeStatus.className = '';
|
|
530
|
-
}
|
|
531
|
-
const confidenceEl = getElementByIdOrNull('detail-confidence');
|
|
532
|
-
if (confidenceEl) {
|
|
533
|
-
confidenceEl.textContent = node.confidence ? `${(node.confidence * 100).toFixed(0)}%` : '-';
|
|
534
|
-
}
|
|
535
|
-
const createdEl = getElementByIdOrNull('detail-created');
|
|
536
|
-
if (createdEl) {
|
|
537
|
-
createdEl.textContent = node.created_at ? new Date(node.created_at).toLocaleString() : '-';
|
|
538
|
-
}
|
|
539
|
-
// Reset reasoning toggle
|
|
540
|
-
const reasoningArrow = getElementByIdOrNull('reasoning-arrow');
|
|
541
|
-
if (reasoningArrow) {
|
|
542
|
-
reasoningArrow.textContent = '▶';
|
|
543
|
-
}
|
|
544
|
-
reasoningEl.classList.add('hidden');
|
|
545
|
-
// Show loading state for similar decisions
|
|
546
|
-
const similarEl = getElementByIdOrNull('detail-similar');
|
|
547
|
-
if (similarEl) {
|
|
548
|
-
similarEl.innerHTML = '<span class="loading-similar">Searching...</span>';
|
|
549
|
-
}
|
|
550
|
-
// Show panel
|
|
551
|
-
panel.classList.add('visible');
|
|
552
|
-
const detail = await API.getGraphDetail(String(node.id));
|
|
553
|
-
const detailNode = detail.node || node;
|
|
554
|
-
const localNode = this.graphData.nodes.find((n) => String(n.id) === String(node.id));
|
|
555
|
-
if (localNode) {
|
|
556
|
-
Object.assign(localNode, detailNode);
|
|
557
|
-
}
|
|
558
|
-
decisionEl.innerHTML = renderSafeMarkdown(String(detailNode.decision || detailNode.decision_preview || '-'));
|
|
559
|
-
reasoningEl.innerHTML = renderSafeMarkdown(String(detailNode.reasoning || '-'));
|
|
560
|
-
// Canonical graph relations are already visible as edges. The old
|
|
561
|
-
// vector-similarity endpoint only understands legacy decision ids.
|
|
562
|
-
if (node.kind) {
|
|
563
|
-
if (similarEl)
|
|
564
|
-
similarEl.textContent = 'Related records are shown as graph edges.';
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
void this.fetchSimilarDecisions(String(node.id));
|
|
568
|
-
}
|
|
569
|
-
logger.debug('showDetail completed successfully');
|
|
570
|
-
}
|
|
571
|
-
catch (error) {
|
|
572
|
-
logger.error('Error in showDetail:', error);
|
|
573
|
-
if (error instanceof Error && error.stack) {
|
|
574
|
-
logger.error('Error stack:', error.stack);
|
|
575
|
-
}
|
|
576
|
-
logger.error('Node data:', node);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Get outcome icon name
|
|
581
|
-
*/
|
|
582
|
-
getOutcomeIcon(outcome) {
|
|
583
|
-
const outcomeMap = {
|
|
584
|
-
pending: 'clock',
|
|
585
|
-
success: 'check-circle',
|
|
586
|
-
failed: 'x-circle',
|
|
587
|
-
partial: 'alert-circle',
|
|
588
|
-
};
|
|
589
|
-
return outcomeMap[(outcome || 'pending').toLowerCase()] || 'clock';
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Toggle reasoning full text
|
|
593
|
-
*/
|
|
594
|
-
toggleReasoning() {
|
|
595
|
-
const arrow = getElementByIdOrNull('reasoning-arrow');
|
|
596
|
-
const content = getElementByIdOrNull('detail-reasoning');
|
|
597
|
-
if (arrow && content) {
|
|
598
|
-
const isHidden = content.classList.contains('hidden');
|
|
599
|
-
content.classList.toggle('hidden');
|
|
600
|
-
arrow.textContent = isHidden ? '▼' : '▶';
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
/**
|
|
604
|
-
* Close detail panel
|
|
605
|
-
*/
|
|
606
|
-
closeDetail() {
|
|
607
271
|
const panel = getElementByIdOrNull('decision-detail-modal');
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
* Fetch similar decisions
|
|
616
|
-
*/
|
|
617
|
-
async fetchSimilarDecisions(nodeId) {
|
|
618
|
-
logger.debug('fetchSimilarDecisions called for node:', nodeId);
|
|
619
|
-
const container = getElementByIdOrNull('detail-similar');
|
|
620
|
-
if (!container) {
|
|
621
|
-
logger.warn('detail-similar element not found');
|
|
272
|
+
const topicEl = getElementByIdOrNull('detail-topic');
|
|
273
|
+
const kindEl = getElementByIdOrNull('detail-kind');
|
|
274
|
+
const decisionEl = getElementByIdOrNull('detail-decision');
|
|
275
|
+
const reasoningEl = getElementByIdOrNull('detail-reasoning');
|
|
276
|
+
const createdEl = getElementByIdOrNull('detail-created');
|
|
277
|
+
if (!panel || !decisionEl || !reasoningEl) {
|
|
278
|
+
logger.error('Detail panel elements missing');
|
|
622
279
|
return;
|
|
623
280
|
}
|
|
281
|
+
if (topicEl)
|
|
282
|
+
topicEl.textContent = node.topic || this.kindLabel(node.kind ?? 'memory');
|
|
283
|
+
if (kindEl)
|
|
284
|
+
kindEl.textContent = this.kindLabel(node.kind ?? 'memory');
|
|
285
|
+
if (createdEl) {
|
|
286
|
+
createdEl.textContent = node.created_at ? new Date(node.created_at).toLocaleString() : '-';
|
|
287
|
+
}
|
|
288
|
+
decisionEl.innerHTML = '<span class="loading-similar">Loading details...</span>';
|
|
289
|
+
reasoningEl.innerHTML = '';
|
|
290
|
+
panel.classList.add('visible');
|
|
291
|
+
const request = ++this.detailRequest;
|
|
624
292
|
try {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
logger.debug('Similar decisions received:', data);
|
|
628
|
-
const similar = (data.similar || []);
|
|
629
|
-
if (similar.length === 0) {
|
|
630
|
-
logger.debug('[MAMA] No similar decisions found');
|
|
631
|
-
container.innerHTML = '<span style="color:#666">No similar decisions found</span>';
|
|
293
|
+
const detail = await API.getGraphDetail(String(node.id));
|
|
294
|
+
if (request !== this.detailRequest)
|
|
632
295
|
return;
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
.map((s) => `
|
|
637
|
-
<button class="similar-decision-btn w-full text-left p-2 mb-2 bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/30 border border-gray-200 dark:border-gray-700 rounded-lg transition-colors" data-node-id="${escapeAttr(String(s.id))}">
|
|
638
|
-
<div class="text-xs font-semibold text-indigo-600 dark:text-indigo-400">${escapeHtml(String(s.topic || ''))}</div>
|
|
639
|
-
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">${escapeHtml(String(s.decision || '').substring(0, 80))}...</div>
|
|
640
|
-
<div class="text-xs text-gray-500 mt-1">${Math.round((s.similarity || 0) * 100)}% match</div>
|
|
641
|
-
</button>
|
|
642
|
-
`)
|
|
643
|
-
.join('');
|
|
644
|
-
logger.debug('[MAMA] Setting similar decisions HTML');
|
|
645
|
-
container.innerHTML = html;
|
|
646
|
-
// Bind click handler once to avoid duplicate listeners.
|
|
647
|
-
container.removeEventListener('click', this.similarDecisionClickHandler);
|
|
648
|
-
container.addEventListener('click', this.similarDecisionClickHandler);
|
|
649
|
-
logger.debug('[MAMA] fetchSimilarDecisions completed');
|
|
650
|
-
}
|
|
651
|
-
catch (error) {
|
|
652
|
-
const message = getErrorMessage(error);
|
|
653
|
-
logger.error('[MAMA] Failed to fetch similar decisions:', message, error);
|
|
654
|
-
if (error instanceof Error && error.stack) {
|
|
655
|
-
logger.error('Error stack:', error.stack);
|
|
656
|
-
}
|
|
657
|
-
container.innerHTML = '<span style="color:#f66">Failed to load</span>';
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* Save outcome for current node
|
|
662
|
-
*/
|
|
663
|
-
async saveOutcome() {
|
|
664
|
-
const current = this.graphData.nodes.find((node) => String(node.id) === this.currentNodeId);
|
|
665
|
-
if ((current?.kind && current.kind !== 'memory') ||
|
|
666
|
-
(current?.state && current.state !== 'current'))
|
|
667
|
-
return;
|
|
668
|
-
const select = getElementByIdOrNull('detail-outcome-select');
|
|
669
|
-
if (!select) {
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
const newOutcome = select.value;
|
|
673
|
-
if (!this.currentNodeId || !newOutcome) {
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
try {
|
|
677
|
-
await API.updateOutcome(this.currentNodeId, newOutcome);
|
|
678
|
-
// Update local data
|
|
679
|
-
const node = this.graphData.nodes.find((n) => String(n.id) === this.currentNodeId);
|
|
680
|
-
if (node) {
|
|
681
|
-
node.outcome = newOutcome.toUpperCase();
|
|
682
|
-
// Update visualization
|
|
683
|
-
this.network.body.data.nodes.update({
|
|
684
|
-
id: this.currentNodeId,
|
|
685
|
-
color: {
|
|
686
|
-
border: this.getOutcomeBorderColor(newOutcome),
|
|
687
|
-
},
|
|
688
|
-
});
|
|
689
|
-
// Refresh detail panel
|
|
690
|
-
this.showDetail(node);
|
|
691
|
-
}
|
|
692
|
-
logger.debug('[MAMA] Outcome updated:', this.currentNodeId, newOutcome);
|
|
296
|
+
const detailNode = detail.node;
|
|
297
|
+
decisionEl.innerHTML = renderSafeMarkdown(String(detailNode.decision || detailNode.decision_preview || '-'));
|
|
298
|
+
reasoningEl.innerHTML = renderSafeMarkdown(String(detailNode.reasoning || '-'));
|
|
693
299
|
}
|
|
694
300
|
catch (error) {
|
|
695
|
-
|
|
696
|
-
logger.error('[MAMA] Failed to update outcome:', message);
|
|
697
|
-
alert(`Failed to update outcome: ${message}`);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
// =============================================
|
|
701
|
-
// Navigation
|
|
702
|
-
// =============================================
|
|
703
|
-
/**
|
|
704
|
-
* Navigate to specific node
|
|
705
|
-
*/
|
|
706
|
-
async navigateToNode(nodeId) {
|
|
707
|
-
if (!this.network) {
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
let nodeIdString = String(nodeId);
|
|
711
|
-
// Try exact match first
|
|
712
|
-
let node = this.graphData.nodes.find((n) => String(n.id) === nodeIdString);
|
|
713
|
-
// If not found, try partial match (for short IDs from checkpoints)
|
|
714
|
-
if (!node) {
|
|
715
|
-
logger.debug('[MAMA] Exact match not found, trying partial match for:', nodeIdString);
|
|
716
|
-
node = this.graphData.nodes.find((n) => String(n.id).startsWith(nodeIdString));
|
|
717
|
-
if (node) {
|
|
718
|
-
logger.debug('[MAMA] Found node via partial match:', node.id);
|
|
719
|
-
nodeIdString = String(node.id); // Update to the full ID
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
// If node not in current graph, reload without filters
|
|
723
|
-
if (!node) {
|
|
724
|
-
logger.warn('[MAMA] Node not in current graph, reloading all nodes...');
|
|
725
|
-
// Reset topic filter
|
|
726
|
-
const topicFilter = getElementByIdOrNull('topic-filter');
|
|
727
|
-
if (topicFilter) {
|
|
728
|
-
topicFilter.value = '';
|
|
729
|
-
}
|
|
730
|
-
// Reload graph: fetch fresh data and reinitialize
|
|
731
|
-
await this.fetchData({ full: true });
|
|
732
|
-
this.init(this.graphData);
|
|
733
|
-
// Try exact match again
|
|
734
|
-
node = this.graphData.nodes.find((n) => String(n.id) === nodeIdString);
|
|
735
|
-
// If still not found, try partial match
|
|
736
|
-
if (!node) {
|
|
737
|
-
node = this.graphData.nodes.find((n) => String(n.id).startsWith(nodeIdString));
|
|
738
|
-
if (node) {
|
|
739
|
-
logger.debug('[MAMA] Found node via partial match after reload:', node.id);
|
|
740
|
-
nodeIdString = String(node.id);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (!node) {
|
|
744
|
-
logger.warn('[MAMA] Node not found even after reload:', nodeIdString);
|
|
745
|
-
showToast('⚠️ Decision not found in graph');
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
// Focus on node (use resolved nodeIdString, not original nodeId)
|
|
750
|
-
this.network.focus(nodeIdString, {
|
|
751
|
-
scale: 1.5,
|
|
752
|
-
animation: { duration: 500, easingFunction: 'easeInOutQuad' },
|
|
753
|
-
});
|
|
754
|
-
// Select node (triggers click event)
|
|
755
|
-
this.network.selectNodes([nodeIdString]);
|
|
756
|
-
// Show detail
|
|
757
|
-
this.showDetail(node);
|
|
758
|
-
this.highlightConnectedNodes(nodeIdString);
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Get connected edge types
|
|
762
|
-
*/
|
|
763
|
-
getConnectedEdges(nodeId) {
|
|
764
|
-
const edges = this.graphData.edges.filter((e) => String(e.from) === nodeId || String(e.to) === nodeId);
|
|
765
|
-
const outgoing = edges.filter((e) => String(e.from) === nodeId);
|
|
766
|
-
const incoming = edges.filter((e) => String(e.to) === nodeId);
|
|
767
|
-
return { outgoing, incoming, all: edges };
|
|
768
|
-
}
|
|
769
|
-
// =============================================
|
|
770
|
-
// Filtering
|
|
771
|
-
// =============================================
|
|
772
|
-
/**
|
|
773
|
-
* Populate topic filter dropdown
|
|
774
|
-
*/
|
|
775
|
-
populateTopicFilter(topics) {
|
|
776
|
-
const datalist = getElementByIdOrNull('topic-filter-list');
|
|
777
|
-
if (!datalist) {
|
|
778
|
-
// Fallback for old select element
|
|
779
|
-
const select = getElementByIdOrNull('topic-filter');
|
|
780
|
-
if (!select)
|
|
301
|
+
if (request !== this.detailRequest)
|
|
781
302
|
return;
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const option = document.createElement('option');
|
|
785
|
-
option.value = topic;
|
|
786
|
-
option.textContent = topic;
|
|
787
|
-
select.appendChild(option);
|
|
788
|
-
});
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
datalist.innerHTML = '';
|
|
792
|
-
topics.forEach((topic) => {
|
|
793
|
-
const option = document.createElement('option');
|
|
794
|
-
option.value = topic;
|
|
795
|
-
datalist.appendChild(option);
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* Populate left-side decision list (time-sorted, newest first)
|
|
800
|
-
*/
|
|
801
|
-
populateDecisionList(nodes) {
|
|
802
|
-
const listEl = getElementByIdOrNull('decision-list');
|
|
803
|
-
const countEl = getElementByIdOrNull('decision-list-count');
|
|
804
|
-
if (!listEl)
|
|
805
|
-
return;
|
|
806
|
-
const sorted = [...nodes].sort((a, b) => {
|
|
807
|
-
const ta = typeof a.created_at === 'number' ? a.created_at : 0;
|
|
808
|
-
const tb = typeof b.created_at === 'number' ? b.created_at : 0;
|
|
809
|
-
return tb - ta;
|
|
810
|
-
});
|
|
811
|
-
if (countEl)
|
|
812
|
-
countEl.textContent = String(sorted.length);
|
|
813
|
-
if (sorted.length === 0) {
|
|
814
|
-
listEl.innerHTML =
|
|
815
|
-
'<div style="padding:16px;text-align:center;color:#9E9891;font-size:11px">No decisions yet</div>';
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
const formatTime = (ts) => {
|
|
819
|
-
if (!ts)
|
|
820
|
-
return '';
|
|
821
|
-
const d = new Date(typeof ts === 'number' ? ts : parseInt(String(ts), 10));
|
|
822
|
-
const now = Date.now();
|
|
823
|
-
const diffH = Math.floor((now - d.getTime()) / 3600000);
|
|
824
|
-
if (diffH < 1)
|
|
825
|
-
return 'just now';
|
|
826
|
-
if (diffH < 24)
|
|
827
|
-
return `${diffH}h ago`;
|
|
828
|
-
const diffD = Math.floor(diffH / 24);
|
|
829
|
-
return `${diffD}d ago`;
|
|
830
|
-
};
|
|
831
|
-
const topicLabel = (topic) => {
|
|
832
|
-
const parts = topic.split('/').filter(Boolean);
|
|
833
|
-
return parts[parts.length - 1] || topic;
|
|
834
|
-
};
|
|
835
|
-
listEl.innerHTML = sorted
|
|
836
|
-
.map((n) => {
|
|
837
|
-
const preview = escapeHtml((n.decision_preview || n.decision || '').slice(0, 80));
|
|
838
|
-
const topic = escapeHtml(topicLabel(n.topic || ''));
|
|
839
|
-
const time = formatTime(n.created_at);
|
|
840
|
-
const id = escapeAttr(String(n.id));
|
|
841
|
-
return `<div class="mama-decision-item" data-node-id="${id}" onclick="window.graphModule?.focusNode('${id}')" style="padding:6px 8px;border-bottom:1px solid #F5F2ED;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='#FFFDF5'" onmouseout="this.style.background=''"><div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:2px"><span style="font-size:11px;font-weight:600;color:#1A1A1A;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px">${topic}</span><span style="font-size:9px;color:#9E9891;flex-shrink:0;font-variant-numeric:tabular-nums">${time}</span></div><div style="font-size:10px;color:#6B6560;line-height:1.4;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical">${preview}</div></div>`;
|
|
842
|
-
})
|
|
843
|
-
.join('');
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Focus a specific node in the graph (called from decision list click)
|
|
847
|
-
*/
|
|
848
|
-
focusNode(nodeId) {
|
|
849
|
-
if (!this.network)
|
|
850
|
-
return;
|
|
851
|
-
try {
|
|
852
|
-
this.network.selectNodes([nodeId]);
|
|
853
|
-
this.network.focus(nodeId, {
|
|
854
|
-
scale: 1.5,
|
|
855
|
-
animation: { duration: 300, easingFunction: 'easeInOutQuad' },
|
|
856
|
-
});
|
|
857
|
-
// Trigger the detail panel
|
|
858
|
-
const nodeData = this.graphData.nodes.find((n) => String(n.id) === nodeId);
|
|
859
|
-
if (nodeData) {
|
|
860
|
-
this.showDetail(nodeData);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
catch (err) {
|
|
864
|
-
logger.warn('focusNode failed:', err);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* Filter by topic
|
|
869
|
-
*/
|
|
870
|
-
filterByTopic(topic) {
|
|
871
|
-
if (!this.network) {
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
const allNodes = this.network.body.data.nodes.get();
|
|
875
|
-
if (!topic) {
|
|
876
|
-
// Show all
|
|
877
|
-
allNodes.forEach((node) => {
|
|
878
|
-
this.network.body.data.nodes.update({
|
|
879
|
-
id: node.id,
|
|
880
|
-
hidden: false,
|
|
881
|
-
});
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
else {
|
|
885
|
-
// Filter — supports exact match from datalist or partial substring match from typing
|
|
886
|
-
const lowerTopic = topic.toLowerCase();
|
|
887
|
-
allNodes.forEach((node) => {
|
|
888
|
-
const nodeData = this.graphData.nodes.find((n) => String(n.id) === String(node.id));
|
|
889
|
-
const nodeTopic = nodeData?.topic || '';
|
|
890
|
-
const matches = nodeTopic === topic || nodeTopic.toLowerCase().includes(lowerTopic);
|
|
891
|
-
this.network.body.data.nodes.update({
|
|
892
|
-
id: node.id,
|
|
893
|
-
hidden: !matches,
|
|
894
|
-
});
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
this.renderLegend();
|
|
898
|
-
logger.debug('[MAMA] Filtered by topic:', topic || 'all');
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* Filter by outcome
|
|
902
|
-
*/
|
|
903
|
-
filterByOutcome(outcome) {
|
|
904
|
-
if (!this.network) {
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
const allNodes = this.network.body.data.nodes.get();
|
|
908
|
-
if (!outcome) {
|
|
909
|
-
// Show all
|
|
910
|
-
allNodes.forEach((node) => {
|
|
911
|
-
this.network.body.data.nodes.update({
|
|
912
|
-
id: node.id,
|
|
913
|
-
hidden: false,
|
|
914
|
-
});
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
else {
|
|
918
|
-
// Filter by outcome
|
|
919
|
-
allNodes.forEach((node) => {
|
|
920
|
-
const nodeData = this.graphData.nodes.find((n) => String(n.id) === String(node.id));
|
|
921
|
-
const nodeOutcome = (nodeData?.outcome || '').toLowerCase();
|
|
922
|
-
this.network.body.data.nodes.update({
|
|
923
|
-
id: node.id,
|
|
924
|
-
hidden: nodeOutcome !== outcome.toLowerCase(),
|
|
925
|
-
});
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
this.renderLegend();
|
|
929
|
-
logger.debug('[MAMA] Filtered by outcome:', outcome || 'all');
|
|
930
|
-
}
|
|
931
|
-
/**
|
|
932
|
-
* Clear all filters
|
|
933
|
-
*/
|
|
934
|
-
clearFilters() {
|
|
935
|
-
if (!this.network) {
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
// Show all nodes
|
|
939
|
-
const allNodes = this.network.body.data.nodes.get();
|
|
940
|
-
allNodes.forEach((node) => {
|
|
941
|
-
this.network.body.data.nodes.update({
|
|
942
|
-
id: node.id,
|
|
943
|
-
hidden: false,
|
|
944
|
-
opacity: 1.0,
|
|
945
|
-
font: { ...node.font, color: '#131313' },
|
|
946
|
-
});
|
|
947
|
-
});
|
|
948
|
-
// Show all edges
|
|
949
|
-
const allEdges = this.network.body.data.edges.get();
|
|
950
|
-
allEdges.forEach((edge) => {
|
|
951
|
-
this.network.body.data.edges.update({
|
|
952
|
-
id: edge.id,
|
|
953
|
-
opacity: 1.0,
|
|
954
|
-
});
|
|
955
|
-
});
|
|
956
|
-
// Clear search state
|
|
957
|
-
this.searchMatches = [];
|
|
958
|
-
this.currentSearchIndex = 0;
|
|
959
|
-
const countEl = getElementByIdOrNull('search-count');
|
|
960
|
-
if (countEl) {
|
|
961
|
-
countEl.style.display = 'none';
|
|
962
|
-
}
|
|
963
|
-
this.renderLegend();
|
|
964
|
-
logger.debug('[MAMA] All filters cleared');
|
|
965
|
-
}
|
|
966
|
-
// =============================================
|
|
967
|
-
// Search
|
|
968
|
-
// =============================================
|
|
969
|
-
/**
|
|
970
|
-
* Perform search
|
|
971
|
-
*/
|
|
972
|
-
search() {
|
|
973
|
-
const queryInput = getElementByIdOrNull('search-input');
|
|
974
|
-
const query = queryInput ? queryInput.value.trim().toLowerCase() : '';
|
|
975
|
-
if (!query) {
|
|
976
|
-
this.clearSearch();
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
// Search in topic, decision, and reasoning
|
|
980
|
-
this.searchMatches = this.graphData.nodes.filter((node) => (node.topic || '').toLowerCase().includes(query) ||
|
|
981
|
-
(node.decision || '').toLowerCase().includes(query) ||
|
|
982
|
-
(node.reasoning || '').toLowerCase().includes(query));
|
|
983
|
-
this.currentSearchIndex = 0;
|
|
984
|
-
this.updateSearchResults();
|
|
985
|
-
if (this.searchMatches.length > 0) {
|
|
986
|
-
this.highlightSearchResults();
|
|
987
|
-
this.navigateToNode(String(this.searchMatches[0].id));
|
|
988
|
-
}
|
|
989
|
-
logger.debug('[MAMA] Search:', query, '- Found', this.searchMatches.length, 'matches');
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* Handle search input
|
|
993
|
-
*/
|
|
994
|
-
handleSearchInput(event) {
|
|
995
|
-
if (event.key === 'Enter' && this.searchMatches.length > 0) {
|
|
996
|
-
this.nextSearchResult();
|
|
997
|
-
}
|
|
998
|
-
else {
|
|
999
|
-
this.debouncedSearch();
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Navigate to next search result
|
|
1004
|
-
*/
|
|
1005
|
-
nextSearchResult() {
|
|
1006
|
-
if (this.searchMatches.length === 0) {
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchMatches.length;
|
|
1010
|
-
this.navigateToNode(this.searchMatches[this.currentSearchIndex].id);
|
|
1011
|
-
this.updateSearchResults();
|
|
1012
|
-
}
|
|
1013
|
-
/**
|
|
1014
|
-
* Navigate to previous search result
|
|
1015
|
-
*/
|
|
1016
|
-
prevSearchResult() {
|
|
1017
|
-
if (this.searchMatches.length === 0) {
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
this.currentSearchIndex =
|
|
1021
|
-
(this.currentSearchIndex - 1 + this.searchMatches.length) % this.searchMatches.length;
|
|
1022
|
-
this.navigateToNode(this.searchMatches[this.currentSearchIndex].id);
|
|
1023
|
-
this.updateSearchResults();
|
|
1024
|
-
}
|
|
1025
|
-
/**
|
|
1026
|
-
* Update search count display
|
|
1027
|
-
*/
|
|
1028
|
-
updateSearchResults() {
|
|
1029
|
-
const countEl = getElementByIdOrNull('search-count');
|
|
1030
|
-
if (!countEl) {
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
if (this.searchMatches.length > 0) {
|
|
1034
|
-
countEl.textContent = `${this.currentSearchIndex + 1} / ${this.searchMatches.length}`;
|
|
1035
|
-
countEl.style.display = 'inline';
|
|
1036
|
-
}
|
|
1037
|
-
else {
|
|
1038
|
-
countEl.textContent = 'No results';
|
|
1039
|
-
countEl.style.display = 'inline';
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* Highlight search results
|
|
1044
|
-
*/
|
|
1045
|
-
highlightSearchResults() {
|
|
1046
|
-
if (!this.network) {
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
const matchIds = this.searchMatches.map((n) => n.id);
|
|
1050
|
-
const allNodes = this.network.body.data.nodes.get();
|
|
1051
|
-
allNodes.forEach((node) => {
|
|
1052
|
-
const isMatch = matchIds.includes(String(node.id));
|
|
1053
|
-
this.network.body.data.nodes.update({
|
|
1054
|
-
id: node.id,
|
|
1055
|
-
opacity: isMatch ? 1.0 : 0.2,
|
|
1056
|
-
font: { ...node.font, color: isMatch ? '#131313' : '#999' },
|
|
1057
|
-
});
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
/**
|
|
1061
|
-
* Clear search
|
|
1062
|
-
*/
|
|
1063
|
-
clearSearch() {
|
|
1064
|
-
this.searchMatches = [];
|
|
1065
|
-
this.currentSearchIndex = 0;
|
|
1066
|
-
this.resetNodeHighlight();
|
|
1067
|
-
const countEl = getElementByIdOrNull('search-count');
|
|
1068
|
-
if (countEl) {
|
|
1069
|
-
countEl.style.display = 'none';
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
/**
|
|
1073
|
-
* Open search panel
|
|
1074
|
-
*/
|
|
1075
|
-
openSearch() {
|
|
1076
|
-
const searchContainer = getElementByIdOrNull('search-container');
|
|
1077
|
-
const searchInput = getElementByIdOrNull('search-input');
|
|
1078
|
-
if (!searchContainer || !searchInput) {
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
searchContainer.style.display = 'flex';
|
|
1082
|
-
searchInput.focus();
|
|
1083
|
-
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Close search panel
|
|
1086
|
-
*/
|
|
1087
|
-
closeSearch() {
|
|
1088
|
-
const searchContainer = getElementByIdOrNull('search-container');
|
|
1089
|
-
if (!searchContainer) {
|
|
1090
|
-
return;
|
|
303
|
+
logger.error('Failed to read record detail:', error);
|
|
304
|
+
decisionEl.textContent = `Could not read this record: ${error instanceof Error ? error.message : String(error)}`;
|
|
1091
305
|
}
|
|
1092
|
-
searchContainer.style.display = 'none';
|
|
1093
|
-
this.clearSearch();
|
|
1094
306
|
}
|
|
1095
307
|
}
|