@abstractframework/monitor-active-memory 0.1.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.
@@ -0,0 +1,1398 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import ReactFlow, { Background, Controls, MarkerType, MiniMap, Panel, ReactFlowProvider } from 'reactflow';
4
+ import { buildKgGraph, buildKgLayout, forceSimulationEnergy, forceSimulationPositions, hashStringToSeed, initForceSimulation, sanitizeViewport, shortestPath, stepForceSimulation, } from './graph';
5
+ import './styles.css';
6
+ function normalizeScope(value, fallback = 'session') {
7
+ const s = String(value ?? '')
8
+ .trim()
9
+ .toLowerCase();
10
+ if (s === 'run' || s === 'session' || s === 'global' || s === 'all')
11
+ return s;
12
+ return fallback;
13
+ }
14
+ function isCompactViewport() {
15
+ if (typeof window === 'undefined')
16
+ return false;
17
+ if (typeof window.matchMedia !== 'function')
18
+ return false;
19
+ try {
20
+ return window.matchMedia('(pointer: coarse)').matches || window.matchMedia('(max-width: 720px)').matches;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ const AMX_LAYOUT_STORAGE_KEY = 'abstractuic_amx_saved_layouts_v1';
27
+ const AMX_VIEWPORT_MIN_ZOOM = 0.025;
28
+ const AMX_VIEWPORT_MAX_ZOOM = 6;
29
+ const AMX_VIEWPORT_MAX_ABS_TRANSLATE = 1000000;
30
+ function safeLocalStorage() {
31
+ if (typeof window === 'undefined')
32
+ return null;
33
+ try {
34
+ return window.localStorage;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function normalizeLayoutKind(value, fallback = 'grid') {
41
+ const v = String(value ?? '')
42
+ .trim()
43
+ .toLowerCase();
44
+ if (v === 'grid' || v === 'circle' || v === 'radial' || v === 'force')
45
+ return v;
46
+ return fallback;
47
+ }
48
+ function coerceXY(value) {
49
+ const obj = value && typeof value === 'object' && !Array.isArray(value) ? value : null;
50
+ if (!obj)
51
+ return null;
52
+ const x = typeof obj.x === 'number' && Number.isFinite(obj.x) ? obj.x : null;
53
+ const y = typeof obj.y === 'number' && Number.isFinite(obj.y) ? obj.y : null;
54
+ if (x === null || y === null)
55
+ return null;
56
+ return { x, y };
57
+ }
58
+ function loadSavedLayout(layoutKey) {
59
+ const storage = safeLocalStorage();
60
+ if (!storage)
61
+ return null;
62
+ const k = String(layoutKey ?? '').trim();
63
+ if (!k)
64
+ return null;
65
+ try {
66
+ const raw = storage.getItem(AMX_LAYOUT_STORAGE_KEY);
67
+ if (!raw)
68
+ return null;
69
+ const parsed = JSON.parse(raw);
70
+ const map = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
71
+ if (!map)
72
+ return null;
73
+ const entry = map[k];
74
+ const obj = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null;
75
+ if (!obj)
76
+ return null;
77
+ const version = obj.version === 1 ? 1 : null;
78
+ if (version === null)
79
+ return null;
80
+ const kind = normalizeLayoutKind(obj.kind, 'grid');
81
+ const seed = typeof obj.seed === 'number' && Number.isFinite(obj.seed) ? Math.trunc(obj.seed) : hashStringToSeed(k);
82
+ const positionsRaw = obj.positions && typeof obj.positions === 'object' && !Array.isArray(obj.positions) ? obj.positions : null;
83
+ const positions = {};
84
+ if (positionsRaw) {
85
+ for (const [id, p] of Object.entries(positionsRaw)) {
86
+ const xy = coerceXY(p);
87
+ if (xy)
88
+ positions[String(id)] = xy;
89
+ }
90
+ }
91
+ const viewport = sanitizeViewport(obj.viewport, {
92
+ minZoom: AMX_VIEWPORT_MIN_ZOOM,
93
+ maxZoom: AMX_VIEWPORT_MAX_ZOOM,
94
+ maxAbsTranslate: AMX_VIEWPORT_MAX_ABS_TRANSLATE,
95
+ }) ?? undefined;
96
+ const saved_at = typeof obj.saved_at === 'string' && obj.saved_at.trim() ? obj.saved_at.trim() : '';
97
+ return { version, kind, seed, positions, viewport, saved_at };
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ function saveLayout(layoutKey, layout) {
104
+ const storage = safeLocalStorage();
105
+ if (!storage)
106
+ return;
107
+ const k = String(layoutKey ?? '').trim();
108
+ if (!k)
109
+ return;
110
+ try {
111
+ const raw = storage.getItem(AMX_LAYOUT_STORAGE_KEY);
112
+ const parsed = raw ? JSON.parse(raw) : null;
113
+ const map = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? { ...parsed } : {};
114
+ map[k] = layout;
115
+ storage.setItem(AMX_LAYOUT_STORAGE_KEY, JSON.stringify(map));
116
+ }
117
+ catch {
118
+ // ignore
119
+ }
120
+ }
121
+ function deleteLayout(layoutKey) {
122
+ const storage = safeLocalStorage();
123
+ if (!storage)
124
+ return;
125
+ const k = String(layoutKey ?? '').trim();
126
+ if (!k)
127
+ return;
128
+ try {
129
+ const raw = storage.getItem(AMX_LAYOUT_STORAGE_KEY);
130
+ if (!raw)
131
+ return;
132
+ const parsed = JSON.parse(raw);
133
+ const map = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? { ...parsed } : null;
134
+ if (!map || !(k in map))
135
+ return;
136
+ delete map[k];
137
+ storage.setItem(AMX_LAYOUT_STORAGE_KEY, JSON.stringify(map));
138
+ }
139
+ catch {
140
+ // ignore
141
+ }
142
+ }
143
+ function parseOptionalFloat(text) {
144
+ const raw = String(text ?? '').trim();
145
+ if (!raw)
146
+ return undefined;
147
+ const n = Number(raw);
148
+ if (!Number.isFinite(n))
149
+ return undefined;
150
+ return n;
151
+ }
152
+ function parseOptionalInt(text, opts) {
153
+ const allowZero = Boolean(opts?.allowZero);
154
+ const allowNegativeOne = Boolean(opts?.allowNegativeOne);
155
+ const raw = String(text ?? '').trim();
156
+ if (!raw)
157
+ return undefined;
158
+ const n = Number(raw);
159
+ if (!Number.isFinite(n))
160
+ return undefined;
161
+ const i = Math.trunc(n);
162
+ if (allowNegativeOne && i === -1)
163
+ return -1;
164
+ if (!allowZero && i <= 0)
165
+ return undefined;
166
+ if (allowZero && i < 0)
167
+ return undefined;
168
+ return i;
169
+ }
170
+ function formatEffort(effort) {
171
+ if (!effort || typeof effort !== 'object' || Array.isArray(effort))
172
+ return null;
173
+ const obj = effort;
174
+ const recallLevel = typeof obj.recall_level === 'string' ? obj.recall_level : '';
175
+ const applied = obj.applied && typeof obj.applied === 'object' && !Array.isArray(obj.applied) ? obj.applied : null;
176
+ const parts = [];
177
+ if (recallLevel)
178
+ parts.push(`recall_level=${recallLevel}`);
179
+ if (applied) {
180
+ if (typeof applied.limit === 'number')
181
+ parts.push(`limit=${applied.limit}`);
182
+ if (typeof applied.min_score === 'number')
183
+ parts.push(`min_score=${applied.min_score}`);
184
+ if (typeof applied.max_input_tokens === 'number')
185
+ parts.push(`max_input_tokens=${applied.max_input_tokens}`);
186
+ }
187
+ return parts.length ? parts.join(' · ') : null;
188
+ }
189
+ function formatAssertionMeta(a) {
190
+ if (!a || typeof a !== 'object')
191
+ return null;
192
+ const parts = [];
193
+ const scope = typeof a.scope === 'string' ? a.scope.trim() : '';
194
+ const owner = typeof a.owner_id === 'string' ? a.owner_id.trim() : '';
195
+ if (scope)
196
+ parts.push(`scope=${scope}`);
197
+ if (owner)
198
+ parts.push(`owner_id=${owner}`);
199
+ const conf = typeof a.confidence === 'number' && Number.isFinite(a.confidence) ? a.confidence : null;
200
+ if (conf !== null)
201
+ parts.push(`confidence=${conf.toFixed(3)}`);
202
+ const prov = a.provenance && typeof a.provenance === 'object' && !Array.isArray(a.provenance) ? a.provenance : null;
203
+ if (prov) {
204
+ const wr = typeof prov.writer_run_id === 'string' ? prov.writer_run_id.trim() : '';
205
+ const wf = typeof prov.writer_workflow_id === 'string' ? prov.writer_workflow_id.trim() : '';
206
+ if (wr)
207
+ parts.push(`writer_run_id=${wr}`);
208
+ if (wf)
209
+ parts.push(`writer_workflow_id=${wf}`);
210
+ }
211
+ const attrs = a.attributes && typeof a.attributes === 'object' && !Array.isArray(a.attributes) ? a.attributes : null;
212
+ const ret = attrs && attrs._retrieval && typeof attrs._retrieval === 'object' && !Array.isArray(attrs._retrieval) ? attrs._retrieval : null;
213
+ const score = ret && typeof ret.score === 'number' && Number.isFinite(ret.score) ? ret.score : null;
214
+ if (score !== null)
215
+ parts.push(`score=${score.toFixed(3)}`);
216
+ return parts.length ? parts.join(' · ') : null;
217
+ }
218
+ function renderHighlight(text, needle) {
219
+ const hay = String(text ?? '');
220
+ const q = String(needle ?? '').trim();
221
+ if (!q)
222
+ return hay;
223
+ const idx = hay.toLowerCase().indexOf(q.toLowerCase());
224
+ if (idx < 0)
225
+ return hay;
226
+ const before = hay.slice(0, idx);
227
+ const match = hay.slice(idx, idx + q.length);
228
+ const after = hay.slice(idx + q.length);
229
+ return (_jsxs(_Fragment, { children: [before, _jsx("span", { className: "amx-hl", children: match }), after] }));
230
+ }
231
+ function kindColor(kind) {
232
+ const k = String(kind ?? '').trim().toLowerCase();
233
+ if (k === 'person')
234
+ return { stroke: 'rgba(96, 165, 250, 0.82)', bg: 'rgba(96, 165, 250, 0.10)', minimap: 'rgba(96, 165, 250, 0.85)' };
235
+ if (k === 'org')
236
+ return { stroke: 'rgba(34, 197, 94, 0.78)', bg: 'rgba(34, 197, 94, 0.10)', minimap: 'rgba(34, 197, 94, 0.82)' };
237
+ if (k === 'concept')
238
+ return { stroke: 'rgba(168, 85, 247, 0.78)', bg: 'rgba(168, 85, 247, 0.10)', minimap: 'rgba(168, 85, 247, 0.82)' };
239
+ if (k === 'claim')
240
+ return { stroke: 'rgba(251, 191, 36, 0.78)', bg: 'rgba(251, 191, 36, 0.10)', minimap: 'rgba(251, 191, 36, 0.82)' };
241
+ if (k === 'event')
242
+ return { stroke: 'rgba(239, 68, 68, 0.74)', bg: 'rgba(239, 68, 68, 0.10)', minimap: 'rgba(239, 68, 68, 0.80)' };
243
+ if (k === 'doc')
244
+ return { stroke: 'rgba(14, 165, 233, 0.74)', bg: 'rgba(14, 165, 233, 0.10)', minimap: 'rgba(14, 165, 233, 0.80)' };
245
+ if (k === 'vocab')
246
+ return { stroke: 'rgba(148, 163, 184, 0.55)', bg: 'rgba(148, 163, 184, 0.06)', minimap: 'rgba(148, 163, 184, 0.70)' };
247
+ if (k === 'thing')
248
+ return { stroke: 'rgba(255, 255, 255, 0.20)', bg: 'rgba(255, 255, 255, 0.04)', minimap: 'rgba(255, 255, 255, 0.30)' };
249
+ return { stroke: 'rgba(255, 255, 255, 0.14)', bg: 'rgba(0, 0, 0, 0.18)', minimap: 'rgba(255, 255, 255, 0.24)' };
250
+ }
251
+ function parseIsoMs(ts) {
252
+ const raw = typeof ts === 'string' ? ts.trim() : '';
253
+ if (!raw)
254
+ return null;
255
+ // Trim microseconds to milliseconds for Date.parse compatibility.
256
+ const normalized = raw.replace(/(\.\d{3})\d+/, '$1');
257
+ const ms = Date.parse(normalized);
258
+ return Number.isFinite(ms) ? ms : null;
259
+ }
260
+ function formatUtcMinute(ts) {
261
+ const ms = parseIsoMs(ts);
262
+ if (ms === null)
263
+ return '';
264
+ const d = new Date(ms);
265
+ const yyyy = d.getUTCFullYear();
266
+ const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
267
+ const dd = String(d.getUTCDate()).padStart(2, '0');
268
+ const hh = String(d.getUTCHours()).padStart(2, '0');
269
+ const mi = String(d.getUTCMinutes()).padStart(2, '0');
270
+ return `${yyyy}/${mm}/${dd} ${hh}:${mi}`;
271
+ }
272
+ function shortTerm(term) {
273
+ const s = String(term ?? '').trim();
274
+ if (!s)
275
+ return '';
276
+ const idx = s.indexOf(':');
277
+ if (idx !== -1 && idx < s.length - 1)
278
+ return s.slice(idx + 1);
279
+ return s;
280
+ }
281
+ function isStructuralPredicate(predicate, structural) {
282
+ const p = String(predicate ?? '')
283
+ .trim()
284
+ .toLowerCase();
285
+ return Boolean(p && structural.has(p));
286
+ }
287
+ function predicateSummary(assertions, opts = {}) {
288
+ const max = Math.max(1, opts.maxPredicates ?? 3);
289
+ const predCounts = new Map();
290
+ for (const a of assertions) {
291
+ const p = String(a?.predicate || '').trim();
292
+ if (!p)
293
+ continue;
294
+ predCounts.set(p, (predCounts.get(p) || 0) + 1);
295
+ }
296
+ const preds = Array.from(predCounts.entries());
297
+ preds.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
298
+ const top = preds.slice(0, max);
299
+ const label = top
300
+ .map(([p, c]) => {
301
+ const term = shortTerm(p);
302
+ return c > 1 ? `${term}×${c}` : term;
303
+ })
304
+ .join(' | ');
305
+ const more = preds.length > max ? ` +${preds.length - max}` : '';
306
+ return `${label}${more}`.trim();
307
+ }
308
+ export function KgActiveMemoryExplorer({ title, resetKey, queryMode, items, activeMemoryText, packets, packetsVersion, packedCount, dropped, estimatedTokens, effort, warnings, onQuery, onItemsReplace, onOpenSpan, onOpenTranscript, }) {
309
+ const flowRef = useRef(null);
310
+ const stepSig = useMemo(() => {
311
+ const first = items?.[0];
312
+ const last = items?.[items.length - 1];
313
+ const f = first ? `${first.subject}|${first.predicate}|${first.object}` : '';
314
+ const l = last ? `${last.subject}|${last.predicate}|${last.object}` : '';
315
+ return `${items?.length || 0}:${f}:${l}`;
316
+ }, [items]);
317
+ const resetSig = String(resetKey ?? stepSig);
318
+ const queryMode2 = queryMode === 'replace' ? 'replace' : 'override';
319
+ const [override, setOverride] = useState(null);
320
+ const [overrideKind, setOverrideKind] = useState(null);
321
+ const [queryLoading, setQueryLoading] = useState(false);
322
+ const [queryError, setQueryError] = useState('');
323
+ const [expandLoading, setExpandLoading] = useState(false);
324
+ const [expandError, setExpandError] = useState('');
325
+ useEffect(() => {
326
+ setOverride(null);
327
+ setOverrideKind(null);
328
+ setQueryError('');
329
+ setQueryLoading(false);
330
+ setExpandError('');
331
+ setExpandLoading(false);
332
+ }, [resetSig]);
333
+ const [search, setSearch] = useState('');
334
+ const [groupEdges, setGroupEdges] = useState(true);
335
+ // Users typically want “connectivity between ideas”, so default to undirected.
336
+ const [directedPath, setDirectedPath] = useState(false);
337
+ const [showStructural, setShowStructural] = useState(true);
338
+ const [miniMapDefaults] = useState(() => {
339
+ const compact = isCompactViewport();
340
+ // Default to collapsed across viewports to keep the graph usable (especially on dense maps).
341
+ return { compact, show: false };
342
+ });
343
+ const compactViewport = miniMapDefaults.compact;
344
+ const [showMiniMap, setShowMiniMap] = useState(miniMapDefaults.show);
345
+ const layoutKey = resetSig;
346
+ const defaultLayoutSeed = useMemo(() => hashStringToSeed(layoutKey), [layoutKey]);
347
+ const [layoutKind, setLayoutKind] = useState('grid');
348
+ const [layoutSeed, setLayoutSeed] = useState(defaultLayoutSeed);
349
+ const [layoutPlaying, setLayoutPlaying] = useState(false);
350
+ const simRef = useRef(null);
351
+ const pendingViewportRef = useRef(null);
352
+ const pendingFitViewRef = useRef(false);
353
+ const [hasSavedLayout, setHasSavedLayout] = useState(false);
354
+ const [savedLayoutAt, setSavedLayoutAt] = useState('');
355
+ const [nodePositions, setNodePositions] = useState({});
356
+ const nodePositionsRef = useRef({});
357
+ useEffect(() => {
358
+ nodePositionsRef.current = nodePositions;
359
+ }, [nodePositions]);
360
+ const [pathStart, setPathStart] = useState('');
361
+ const [pathEnd, setPathEnd] = useState('');
362
+ const inferredScope = useMemo(() => {
363
+ const s = items?.[0]?.scope;
364
+ return normalizeScope(s, 'session');
365
+ }, [items]);
366
+ const [scope, setScope] = useState(inferredScope);
367
+ const [recallLevel, setRecallLevel] = useState('standard');
368
+ const [queryText, setQueryText] = useState('');
369
+ const [subject, setSubject] = useState('');
370
+ const [predicate, setPredicate] = useState('');
371
+ const [object, setObject] = useState('');
372
+ const [minScore, setMinScore] = useState('');
373
+ const [limit, setLimit] = useState('');
374
+ const [maxInputTokens, setMaxInputTokens] = useState('');
375
+ const [model, setModel] = useState('qwen/qwen3-next-80b');
376
+ // If the "context" changes (e.g. different step/run), default scope to match.
377
+ // For streaming use-cases, set `resetKey` so scope doesn't flap as items update.
378
+ useEffect(() => {
379
+ setScope(inferredScope);
380
+ // eslint-disable-next-line react-hooks/exhaustive-deps
381
+ }, [resetSig]);
382
+ const stepItems = Array.isArray(items) ? items : [];
383
+ const stepActiveMemoryText = typeof activeMemoryText === 'string' ? activeMemoryText : '';
384
+ const stepPackets = Array.isArray(packets) ? packets : [];
385
+ const stepPacketsVersion = typeof packetsVersion === 'number' ? packetsVersion : undefined;
386
+ const stepPackedCount = typeof packedCount === 'number' ? packedCount : undefined;
387
+ const stepDropped = typeof dropped === 'number' ? dropped : undefined;
388
+ const stepEstimatedTokens = typeof estimatedTokens === 'number' ? estimatedTokens : undefined;
389
+ const stepEffort = effort;
390
+ const stepWarnings = warnings;
391
+ const displayItems = queryMode2 === 'override' && override?.items && Array.isArray(override.items) ? override.items : stepItems;
392
+ const displayActiveMemoryText = typeof override?.active_memory_text === 'string' && override.active_memory_text.trim()
393
+ ? override.active_memory_text
394
+ : stepActiveMemoryText;
395
+ const displayEffort = override?.effort ?? stepEffort;
396
+ const displayWarnings = override?.warnings ?? stepWarnings;
397
+ const structuralPredicates = useMemo(() => new Set([
398
+ 'rdf:type',
399
+ 'schema:name',
400
+ 'skos:preflabel',
401
+ 'skos:altlabel',
402
+ 'dcterms:title',
403
+ 'dcterms:identifier',
404
+ ]), []);
405
+ const visibleItems = useMemo(() => {
406
+ if (showStructural)
407
+ return displayItems;
408
+ return (displayItems || []).filter((a) => !isStructuralPredicate(a?.predicate, structuralPredicates));
409
+ }, [displayItems, showStructural, structuralPredicates]);
410
+ const baseGraph = useMemo(() => buildKgGraph(displayItems, { groupEdges }), [displayItems, groupEdges]);
411
+ const graph = useMemo(() => {
412
+ if (showStructural)
413
+ return baseGraph;
414
+ const edges = [];
415
+ for (const e of baseGraph.edges) {
416
+ const assertionsRaw = e && e.data && Array.isArray(e.data.assertions) ? e.data.assertions : [];
417
+ const assertions = assertionsRaw.filter((a) => !isStructuralPredicate(a?.predicate, structuralPredicates));
418
+ if (!assertions.length)
419
+ continue;
420
+ const summary = predicateSummary(assertions);
421
+ edges.push({
422
+ ...e,
423
+ label: summary,
424
+ data: {
425
+ ...e.data,
426
+ assertions,
427
+ predicateSummary: summary,
428
+ },
429
+ });
430
+ }
431
+ return { nodes: baseGraph.nodes, edges };
432
+ }, [baseGraph, showStructural, structuralPredicates]);
433
+ const nodeIds = useMemo(() => graph.nodes.map((n) => n.id), [graph.nodes]);
434
+ const nodeIdsSig = useMemo(() => {
435
+ const first = nodeIds[0] || '';
436
+ const last = nodeIds[nodeIds.length - 1] || '';
437
+ return `${nodeIds.length}:${first}:${last}`;
438
+ }, [nodeIds]);
439
+ const nodeLabelById = useMemo(() => {
440
+ const m = new Map();
441
+ for (const n of graph.nodes)
442
+ m.set(n.id, String(n.data?.label || n.id));
443
+ return m;
444
+ }, [graph.nodes]);
445
+ const nodeKindById = useMemo(() => {
446
+ const m = new Map();
447
+ for (const n of graph.nodes)
448
+ m.set(n.id, String(n.data?.kind || ''));
449
+ return m;
450
+ }, [graph.nodes]);
451
+ const searchKey = search.trim().toLowerCase();
452
+ const matchedNodeIds = useMemo(() => {
453
+ if (!searchKey)
454
+ return new Set();
455
+ const set = new Set();
456
+ for (const n of graph.nodes) {
457
+ const id = String(n.id || '').toLowerCase();
458
+ const label = String(n.data?.label || '').toLowerCase();
459
+ if (id.includes(searchKey) || label.includes(searchKey))
460
+ set.add(n.id);
461
+ }
462
+ return set;
463
+ }, [graph.nodes, searchKey]);
464
+ const applyPositionsToNodes = useCallback((positions, fallback) => {
465
+ const next = {};
466
+ for (const id of nodeIds) {
467
+ const p = positions[id] || fallback[id];
468
+ next[id] = p ? { x: p.x, y: p.y } : { x: 0, y: 0 };
469
+ }
470
+ return next;
471
+ }, [nodeIds]);
472
+ useEffect(() => {
473
+ // Ensure all current nodes have a position (preserve user-dragged positions).
474
+ setNodePositions((prev) => {
475
+ const fallback = buildKgLayout(graph, { kind: layoutKind, seed: layoutSeed });
476
+ const next = {};
477
+ for (const id of nodeIds) {
478
+ const p = prev[id] || fallback[id];
479
+ next[id] = p ? { x: p.x, y: p.y } : { x: 0, y: 0 };
480
+ }
481
+ return next;
482
+ });
483
+ }, [graph, layoutKind, layoutSeed, nodeIds, nodeIdsSig]);
484
+ useEffect(() => {
485
+ if (!pendingFitViewRef.current)
486
+ return;
487
+ const inst = flowRef.current;
488
+ if (!inst || typeof inst.fitView !== 'function')
489
+ return;
490
+ if (!nodeIds.length)
491
+ return;
492
+ pendingFitViewRef.current = false;
493
+ try {
494
+ inst.fitView({ padding: 0.2, duration: 0 });
495
+ }
496
+ catch {
497
+ try {
498
+ inst.fitView({ padding: 0.2 });
499
+ }
500
+ catch {
501
+ // ignore
502
+ }
503
+ }
504
+ }, [nodeIds.length, nodeIdsSig, nodePositions]);
505
+ useEffect(() => {
506
+ // Load saved layout for this view key (if present). Falls back to a deterministic layout.
507
+ setLayoutPlaying(false);
508
+ simRef.current = null;
509
+ pendingViewportRef.current = null;
510
+ pendingFitViewRef.current = false;
511
+ const saved = loadSavedLayout(layoutKey);
512
+ if (saved && Object.keys(saved.positions || {}).length) {
513
+ setLayoutKind(saved.kind);
514
+ setLayoutSeed(saved.seed);
515
+ setHasSavedLayout(true);
516
+ setSavedLayoutAt(saved.saved_at || '');
517
+ if (saved.viewport) {
518
+ pendingViewportRef.current = saved.viewport;
519
+ const inst = flowRef.current;
520
+ if (inst && typeof inst.setViewport === 'function') {
521
+ try {
522
+ inst.setViewport(saved.viewport, { duration: 0 });
523
+ }
524
+ catch {
525
+ try {
526
+ inst.setViewport(saved.viewport);
527
+ }
528
+ catch {
529
+ // ignore
530
+ }
531
+ }
532
+ pendingViewportRef.current = null;
533
+ }
534
+ }
535
+ else {
536
+ pendingFitViewRef.current = true;
537
+ }
538
+ const fallback = buildKgLayout(graph, { kind: saved.kind, seed: saved.seed });
539
+ setNodePositions(applyPositionsToNodes(saved.positions, fallback));
540
+ return;
541
+ }
542
+ setHasSavedLayout(false);
543
+ setSavedLayoutAt('');
544
+ const seed = hashStringToSeed(layoutKey);
545
+ setLayoutSeed(seed);
546
+ const fallback = buildKgLayout(graph, { kind: layoutKind, seed });
547
+ setNodePositions(fallback);
548
+ // If the user chose a force layout but hasn't saved one, auto-run a short stabilization pass.
549
+ if (layoutKind === 'force' && graph.nodes.length > 0 && graph.nodes.length <= 320) {
550
+ simRef.current = initForceSimulation(graph, { seed, positions: fallback });
551
+ setLayoutPlaying(true);
552
+ }
553
+ // eslint-disable-next-line react-hooks/exhaustive-deps
554
+ }, [layoutKey]);
555
+ const path = useMemo(() => {
556
+ const s = String(pathStart || '').trim();
557
+ const e = String(pathEnd || '').trim();
558
+ if (!s || !e)
559
+ return null;
560
+ return shortestPath(graph, s, e, { directed: directedPath });
561
+ }, [graph, pathStart, pathEnd, directedPath]);
562
+ const isPathFocus = Boolean(path && String(pathStart || '').trim() && String(pathEnd || '').trim());
563
+ const noPathDiagnostics = useMemo(() => {
564
+ const s = String(pathStart || '').trim();
565
+ const e = String(pathEnd || '').trim();
566
+ if (!s || !e)
567
+ return null;
568
+ const directed = directedPath;
569
+ const adj = new Map();
570
+ const add = (from, to) => {
571
+ const cur = adj.get(from);
572
+ if (cur)
573
+ cur.push(to);
574
+ else
575
+ adj.set(from, [to]);
576
+ };
577
+ for (const edge of graph.edges) {
578
+ add(edge.source, edge.target);
579
+ if (!directed)
580
+ add(edge.target, edge.source);
581
+ }
582
+ const visited = new Set();
583
+ const q = [];
584
+ visited.add(s);
585
+ q.push(s);
586
+ while (q.length) {
587
+ const cur = q.shift();
588
+ const neigh = adj.get(cur) || [];
589
+ for (const n of neigh) {
590
+ if (visited.has(n))
591
+ continue;
592
+ visited.add(n);
593
+ q.push(n);
594
+ }
595
+ }
596
+ return {
597
+ reachableFromStart: visited.size,
598
+ endReachable: visited.has(e),
599
+ nodes: graph.nodes.length,
600
+ edges: graph.edges.length,
601
+ assertions: visibleItems.length,
602
+ };
603
+ }, [directedPath, graph.edges, graph.nodes.length, pathEnd, pathStart, visibleItems.length]);
604
+ const selectedPathNodeSet = useMemo(() => new Set(path?.nodeIds || []), [path]);
605
+ const selectedPathEdgeSet = useMemo(() => new Set(path?.edgeIds || []), [path]);
606
+ const [selectedNodeId, setSelectedNodeId] = useState('');
607
+ const [selectedEdgeId, setSelectedEdgeId] = useState('');
608
+ const selectionActive = Boolean(String(selectedNodeId || '').trim() || String(selectedEdgeId || '').trim());
609
+ const selectionNeighborhood = useMemo(() => {
610
+ const nodes = new Set();
611
+ const edges = new Set();
612
+ const nid = String(selectedNodeId || '').trim();
613
+ const eid = String(selectedEdgeId || '').trim();
614
+ if (nid) {
615
+ nodes.add(nid);
616
+ for (const e of graph.edges) {
617
+ if (e.source === nid || e.target === nid) {
618
+ edges.add(e.id);
619
+ nodes.add(e.source);
620
+ nodes.add(e.target);
621
+ }
622
+ }
623
+ return { nodes, edges };
624
+ }
625
+ if (eid) {
626
+ const e = graph.edges.find((x) => x.id === eid);
627
+ if (e) {
628
+ edges.add(e.id);
629
+ nodes.add(e.source);
630
+ nodes.add(e.target);
631
+ }
632
+ }
633
+ return { nodes, edges };
634
+ }, [graph.edges, selectedEdgeId, selectedNodeId]);
635
+ useEffect(() => {
636
+ setSelectedNodeId('');
637
+ setSelectedEdgeId('');
638
+ setPathStart('');
639
+ setPathEnd('');
640
+ }, [resetSig]);
641
+ const [showPackets, setShowPackets] = useState(false);
642
+ useEffect(() => {
643
+ setShowPackets(false);
644
+ }, [resetSig]);
645
+ useEffect(() => {
646
+ if (!layoutPlaying) {
647
+ simRef.current = null;
648
+ }
649
+ }, [layoutPlaying]);
650
+ useEffect(() => {
651
+ if (layoutKind === 'force')
652
+ return;
653
+ if (layoutPlaying)
654
+ setLayoutPlaying(false);
655
+ }, [layoutKind, layoutPlaying]);
656
+ useEffect(() => {
657
+ if (!layoutPlaying)
658
+ return;
659
+ if (layoutKind !== 'force')
660
+ return;
661
+ if (graph.nodes.length === 0)
662
+ return;
663
+ if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function')
664
+ return;
665
+ if (!simRef.current) {
666
+ simRef.current = initForceSimulation(graph, { seed: layoutSeed, positions: nodePositionsRef.current });
667
+ }
668
+ let raf = 0;
669
+ let lastTs = 0;
670
+ let ticks = 0;
671
+ const stepsPerFrame = graph.nodes.length <= 140 ? 2 : 1;
672
+ const maxTicks = 1600;
673
+ const energyThreshold = 0.08;
674
+ const loop = (ts) => {
675
+ raf = window.requestAnimationFrame(loop);
676
+ if (ts - lastTs < 33)
677
+ return; // ~30fps cap
678
+ lastTs = ts;
679
+ const sim = simRef.current;
680
+ if (!sim) {
681
+ setLayoutPlaying(false);
682
+ return;
683
+ }
684
+ stepForceSimulation(sim, stepsPerFrame);
685
+ ticks += stepsPerFrame;
686
+ setNodePositions(forceSimulationPositions(sim));
687
+ const energy = forceSimulationEnergy(sim);
688
+ if (energy <= energyThreshold || ticks >= maxTicks) {
689
+ setLayoutPlaying(false);
690
+ }
691
+ };
692
+ raf = window.requestAnimationFrame(loop);
693
+ return () => {
694
+ window.cancelAnimationFrame(raf);
695
+ };
696
+ }, [graph, layoutKind, layoutPlaying, layoutSeed]);
697
+ const nodes = useMemo(() => {
698
+ return graph.nodes.map((n) => {
699
+ const isPath = selectedPathNodeSet.has(n.id);
700
+ const isMatch = matchedNodeIds.has(n.id);
701
+ const isSelected = selectedNodeId === n.id;
702
+ const isNeighborhood = selectionActive && selectionNeighborhood.nodes.has(n.id);
703
+ const base = kindColor(n.data?.kind);
704
+ const matchBorder = 'rgba(34, 211, 238, 0.88)';
705
+ const matchGlow = isMatch
706
+ ? '0 0 0 1px rgba(34, 211, 238, 0.34), 0 0 16px rgba(34, 211, 238, 0.26), 0 0 44px rgba(34, 211, 238, 0.14)'
707
+ : '';
708
+ const borderColor = isPath ? 'rgba(168, 85, 247, 0.9)' : isMatch ? matchBorder : base.stroke;
709
+ const baseShadow = isSelected
710
+ ? '0 0 0 2px rgba(255,255,255,0.20), 0 18px 46px rgba(0,0,0,0.55)'
711
+ : isNeighborhood
712
+ ? '0 0 0 1px rgba(96, 165, 250, 0.28), 0 12px 34px rgba(0,0,0,0.45)'
713
+ : undefined;
714
+ const shadow = matchGlow && baseShadow ? `${matchGlow}, ${baseShadow}` : baseShadow || (matchGlow || undefined);
715
+ const bg = isPath ? 'rgba(168, 85, 247, 0.10)' : isMatch ? 'rgba(34, 211, 238, 0.08)' : base.bg;
716
+ const opacity = isPathFocus
717
+ ? isPath || isSelected
718
+ ? 1
719
+ : 0.16
720
+ : selectionActive
721
+ ? isNeighborhood || isSelected
722
+ ? 1
723
+ : 0.10
724
+ : 1;
725
+ return {
726
+ ...n,
727
+ position: nodePositions[n.id] ? { x: nodePositions[n.id].x, y: nodePositions[n.id].y } : n.position,
728
+ style: {
729
+ ...(n.style || {}),
730
+ border: `1px solid ${borderColor}`,
731
+ borderRadius: 10,
732
+ background: bg,
733
+ color: 'rgba(255,255,255,0.92)',
734
+ opacity,
735
+ padding: 8,
736
+ fontSize: 12,
737
+ boxShadow: shadow,
738
+ width: 180,
739
+ cursor: 'pointer',
740
+ },
741
+ };
742
+ });
743
+ }, [graph.nodes, isPathFocus, matchedNodeIds, nodePositions, selectedNodeId, selectedPathNodeSet, selectionActive, selectionNeighborhood.nodes]);
744
+ const edges = useMemo(() => {
745
+ return graph.edges.map((e) => {
746
+ const isPath = selectedPathEdgeSet.has(e.id);
747
+ const isSelected = selectedEdgeId === e.id;
748
+ const isNeighborhood = selectionActive && selectionNeighborhood.edges.has(e.id);
749
+ const stroke = isPath
750
+ ? 'rgba(168, 85, 247, 0.9)'
751
+ : selectionActive
752
+ ? isNeighborhood || isSelected
753
+ ? 'rgba(96, 165, 250, 0.55)'
754
+ : 'rgba(255,255,255,0.14)'
755
+ : 'rgba(255,255,255,0.18)';
756
+ const width = isPath ? 2.75 : selectionActive ? (isNeighborhood || isSelected ? 2.25 : 1.25) : 1.5;
757
+ const glow = isSelected ? 'drop-shadow(0 0 10px rgba(168, 85, 247, 0.25))' : undefined;
758
+ const markerColor = isPath ? 'rgba(168, 85, 247, 0.9)' : isPathFocus ? 'rgba(255,255,255,0.10)' : 'rgba(255,255,255,0.28)';
759
+ const opacity = isPathFocus
760
+ ? isPath || isSelected
761
+ ? 1
762
+ : 0.10
763
+ : selectionActive
764
+ ? isNeighborhood || isSelected
765
+ ? 1
766
+ : 0.08
767
+ : 1;
768
+ const labelOpacity = isPathFocus ? (isPath || isSelected ? 1 : 0) : selectionActive ? (isNeighborhood || isSelected ? 1 : 0) : 1;
769
+ const label = isPathFocus ? (isPath || isSelected ? String(e.label || '') : '') : String(e.label || '');
770
+ return {
771
+ ...e,
772
+ label,
773
+ style: {
774
+ ...(e.style || {}),
775
+ stroke,
776
+ strokeWidth: width,
777
+ filter: glow,
778
+ opacity,
779
+ },
780
+ animated: false,
781
+ markerEnd: { type: MarkerType.ArrowClosed, color: markerColor, width: 20, height: 20 },
782
+ labelShowBg: true,
783
+ labelBgPadding: [6, 3],
784
+ labelBgBorderRadius: 10,
785
+ labelBgStyle: {
786
+ fill: `rgba(0, 0, 0, ${0.38 * labelOpacity})`,
787
+ stroke: `rgba(255, 255, 255, ${0.14 * labelOpacity})`,
788
+ strokeWidth: 1,
789
+ },
790
+ labelStyle: {
791
+ fill: `rgba(255, 255, 255, ${0.92 * labelOpacity})`,
792
+ fontSize: 10,
793
+ fontWeight: 600,
794
+ letterSpacing: 0.2,
795
+ textTransform: 'lowercase',
796
+ },
797
+ };
798
+ });
799
+ }, [graph.edges, isPathFocus, selectedEdgeId, selectedPathEdgeSet, selectionActive, selectionNeighborhood.edges]);
800
+ const selectedEdgeAssertions = useMemo(() => {
801
+ if (!selectedEdgeId)
802
+ return [];
803
+ const e = graph.edges.find((x) => x.id === selectedEdgeId);
804
+ const a = e && e.data && Array.isArray(e.data.assertions) ? e.data.assertions : [];
805
+ return Array.isArray(a) ? a : [];
806
+ }, [graph.edges, selectedEdgeId]);
807
+ const selectedEdge = useMemo(() => {
808
+ if (!selectedEdgeId)
809
+ return null;
810
+ return graph.edges.find((x) => x.id === selectedEdgeId) || null;
811
+ }, [graph.edges, selectedEdgeId]);
812
+ const selectedNodeAssertions = useMemo(() => {
813
+ const id = String(selectedNodeId || '').trim();
814
+ if (!id)
815
+ return [];
816
+ return displayItems.filter((a) => {
817
+ if (!a || typeof a !== 'object')
818
+ return false;
819
+ if (!(a.subject === id || a.object === id))
820
+ return false;
821
+ if (showStructural)
822
+ return true;
823
+ return !isStructuralPredicate(a.predicate, structuralPredicates);
824
+ });
825
+ }, [displayItems, selectedNodeId, showStructural, structuralPredicates]);
826
+ const selectedAssertions = useMemo(() => {
827
+ return selectedEdgeId ? selectedEdgeAssertions : selectedNodeAssertions;
828
+ }, [selectedEdgeAssertions, selectedEdgeId, selectedNodeAssertions]);
829
+ const selectedSources = useMemo(() => {
830
+ const out = new Map();
831
+ for (const a of selectedAssertions) {
832
+ if (!a || typeof a !== 'object')
833
+ continue;
834
+ const prov = a.provenance && typeof a.provenance === 'object' && !Array.isArray(a.provenance) ? a.provenance : null;
835
+ const spanId = prov && typeof prov.span_id === 'string' ? prov.span_id.trim() : '';
836
+ const writerRunId = prov && typeof prov.writer_run_id === 'string' ? prov.writer_run_id.trim() : '';
837
+ const ownerId = typeof a.owner_id === 'string' ? a.owner_id.trim() : '';
838
+ const spanRunId = writerRunId || ownerId;
839
+ if (!spanRunId || !spanId)
840
+ continue;
841
+ const key = `${spanRunId}|${spanId}`;
842
+ const cur = out.get(key);
843
+ const obs = typeof a.observed_at === 'string' ? a.observed_at.trim() : '';
844
+ if (cur) {
845
+ cur.count += 1;
846
+ if (obs && (!cur.last_observed_at || obs > cur.last_observed_at)) {
847
+ cur.last_observed_at = obs;
848
+ cur.last_observed_at_fmt = formatUtcMinute(obs);
849
+ cur.assertion = a;
850
+ }
851
+ }
852
+ else {
853
+ out.set(key, {
854
+ span_run_id: spanRunId,
855
+ span_id: spanId,
856
+ writer_run_id: writerRunId,
857
+ count: 1,
858
+ last_observed_at: obs,
859
+ last_observed_at_fmt: formatUtcMinute(obs),
860
+ assertion: a,
861
+ });
862
+ }
863
+ }
864
+ return Array.from(out.values()).sort((a, b) => String(b.last_observed_at || '').localeCompare(String(a.last_observed_at || '')));
865
+ }, [selectedAssertions]);
866
+ const selectedRunTranscripts = useMemo(() => {
867
+ const out = new Map();
868
+ for (const a of selectedAssertions) {
869
+ if (!a || typeof a !== 'object')
870
+ continue;
871
+ const prov = a.provenance && typeof a.provenance === 'object' && !Array.isArray(a.provenance) ? a.provenance : null;
872
+ const spanId = prov && typeof prov.span_id === 'string' ? prov.span_id.trim() : '';
873
+ const writerRunId = prov && typeof prov.writer_run_id === 'string' ? prov.writer_run_id.trim() : '';
874
+ if (!writerRunId)
875
+ continue;
876
+ if (spanId)
877
+ continue;
878
+ const cur = out.get(writerRunId);
879
+ const obs = typeof a.observed_at === 'string' ? a.observed_at.trim() : '';
880
+ if (cur) {
881
+ cur.count += 1;
882
+ if (obs && (!cur.last_observed_at || obs > cur.last_observed_at)) {
883
+ cur.last_observed_at = obs;
884
+ cur.last_observed_at_fmt = formatUtcMinute(obs);
885
+ cur.assertion = a;
886
+ }
887
+ }
888
+ else {
889
+ out.set(writerRunId, {
890
+ run_id: writerRunId,
891
+ count: 1,
892
+ last_observed_at: obs,
893
+ last_observed_at_fmt: formatUtcMinute(obs),
894
+ assertion: a,
895
+ });
896
+ }
897
+ }
898
+ return Array.from(out.values()).sort((a, b) => String(b.last_observed_at || '').localeCompare(String(a.last_observed_at || '')));
899
+ }, [selectedAssertions]);
900
+ const openTranscript = useCallback((args) => {
901
+ const run_id = String(args.run_id || '').trim();
902
+ const span_id = typeof args.span_id === 'string' ? String(args.span_id).trim() : '';
903
+ if (!run_id)
904
+ return;
905
+ if (onOpenTranscript) {
906
+ try {
907
+ onOpenTranscript({ run_id, span_id: span_id || undefined, assertion: args.assertion });
908
+ }
909
+ catch {
910
+ // Best-effort.
911
+ }
912
+ return;
913
+ }
914
+ if (onOpenSpan && span_id) {
915
+ try {
916
+ onOpenSpan({ run_id, span_id, assertion: args.assertion });
917
+ }
918
+ catch {
919
+ // Best-effort.
920
+ }
921
+ }
922
+ }, [onOpenSpan, onOpenTranscript]);
923
+ const copyText = useCallback(async (text) => {
924
+ const s = String(text ?? '');
925
+ if (!s)
926
+ return;
927
+ try {
928
+ await navigator.clipboard.writeText(s);
929
+ }
930
+ catch {
931
+ // Best-effort.
932
+ }
933
+ }, []);
934
+ const fitSelected = useCallback(() => {
935
+ const inst = flowRef.current;
936
+ if (!inst || typeof inst.fitView !== 'function')
937
+ return;
938
+ if (selectedNodeId && typeof inst.getNode === 'function') {
939
+ const n = inst.getNode(selectedNodeId);
940
+ if (n) {
941
+ inst.fitView({ nodes: [n], padding: 0.55, duration: 350 });
942
+ return;
943
+ }
944
+ }
945
+ inst.fitView({ padding: 0.2, duration: 350 });
946
+ }, [selectedNodeId]);
947
+ const applyLayoutNow = useCallback((next) => {
948
+ const kind = next.kind;
949
+ const seed = Math.trunc(next.seed);
950
+ setLayoutPlaying(false);
951
+ simRef.current = null;
952
+ pendingViewportRef.current = null;
953
+ setLayoutKind(kind);
954
+ setLayoutSeed(seed);
955
+ const positions = buildKgLayout(graph, { kind, seed });
956
+ setNodePositions(positions);
957
+ if (kind === 'force' && (next.autoPlay ?? true) && graph.nodes.length > 0 && graph.nodes.length <= 320) {
958
+ simRef.current = initForceSimulation(graph, { seed, positions });
959
+ setLayoutPlaying(true);
960
+ }
961
+ }, [graph]);
962
+ const toggleSimulation = useCallback(() => {
963
+ if (layoutKind !== 'force')
964
+ return;
965
+ if (layoutPlaying) {
966
+ setLayoutPlaying(false);
967
+ return;
968
+ }
969
+ simRef.current = initForceSimulation(graph, { seed: layoutSeed, positions: nodePositionsRef.current });
970
+ setLayoutPlaying(true);
971
+ }, [graph, layoutKind, layoutPlaying, layoutSeed]);
972
+ const saveLayoutNow = useCallback(() => {
973
+ const now = new Date().toISOString();
974
+ const positions = {};
975
+ for (const id of nodeIds) {
976
+ const p = nodePositionsRef.current[id];
977
+ if (p && typeof p.x === 'number' && typeof p.y === 'number')
978
+ positions[id] = { x: p.x, y: p.y };
979
+ }
980
+ const inst = flowRef.current;
981
+ const vpObj = inst && typeof inst.toObject === 'function' ? inst.toObject() : null;
982
+ const viewportRaw = vpObj && vpObj.viewport ? vpObj.viewport : inst && typeof inst.getViewport === 'function' ? inst.getViewport() : null;
983
+ const viewport = sanitizeViewport(viewportRaw, {
984
+ minZoom: AMX_VIEWPORT_MIN_ZOOM,
985
+ maxZoom: AMX_VIEWPORT_MAX_ZOOM,
986
+ maxAbsTranslate: AMX_VIEWPORT_MAX_ABS_TRANSLATE,
987
+ }) ?? undefined;
988
+ saveLayout(layoutKey, { version: 1, kind: layoutKind, seed: layoutSeed, positions, viewport, saved_at: now });
989
+ setHasSavedLayout(true);
990
+ setSavedLayoutAt(now);
991
+ setLayoutPlaying(false);
992
+ }, [layoutKey, layoutKind, layoutSeed, nodeIds]);
993
+ const loadSavedLayoutNow = useCallback(() => {
994
+ const saved = loadSavedLayout(layoutKey);
995
+ if (!saved)
996
+ return;
997
+ setLayoutPlaying(false);
998
+ simRef.current = null;
999
+ setLayoutKind(saved.kind);
1000
+ setLayoutSeed(saved.seed);
1001
+ setHasSavedLayout(true);
1002
+ setSavedLayoutAt(saved.saved_at || '');
1003
+ pendingViewportRef.current = saved.viewport || null;
1004
+ pendingFitViewRef.current = !saved.viewport;
1005
+ const fallback = buildKgLayout(graph, { kind: saved.kind, seed: saved.seed });
1006
+ setNodePositions(applyPositionsToNodes(saved.positions, fallback));
1007
+ if (saved.viewport) {
1008
+ const inst = flowRef.current;
1009
+ if (inst && typeof inst.setViewport === 'function') {
1010
+ try {
1011
+ inst.setViewport(saved.viewport, { duration: 0 });
1012
+ }
1013
+ catch {
1014
+ try {
1015
+ inst.setViewport(saved.viewport);
1016
+ }
1017
+ catch {
1018
+ // ignore
1019
+ }
1020
+ }
1021
+ pendingViewportRef.current = null;
1022
+ }
1023
+ }
1024
+ }, [applyPositionsToNodes, graph, layoutKey]);
1025
+ const clearSavedLayoutNow = useCallback(() => {
1026
+ deleteLayout(layoutKey);
1027
+ setHasSavedLayout(false);
1028
+ setSavedLayoutAt('');
1029
+ }, [layoutKey]);
1030
+ const onNodesChange = useCallback((changes) => {
1031
+ if (!Array.isArray(changes) || changes.length === 0)
1032
+ return;
1033
+ let sawDrag = false;
1034
+ setNodePositions((prev) => {
1035
+ let next = prev;
1036
+ let changed = false;
1037
+ for (const ch of changes) {
1038
+ const id = ch && typeof ch === 'object' && 'id' in ch && typeof ch.id === 'string' ? String(ch.id) : '';
1039
+ const pos = ch?.type === 'position' ? ch?.position : null;
1040
+ if (id && pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
1041
+ if (!changed) {
1042
+ next = { ...prev };
1043
+ changed = true;
1044
+ }
1045
+ next[id] = { x: pos.x, y: pos.y };
1046
+ if (ch?.dragging)
1047
+ sawDrag = true;
1048
+ }
1049
+ }
1050
+ return changed ? next : prev;
1051
+ });
1052
+ if (sawDrag && layoutPlaying)
1053
+ setLayoutPlaying(false);
1054
+ }, [layoutPlaying]);
1055
+ const edgeById = useMemo(() => {
1056
+ const m = new Map();
1057
+ for (const e of graph.edges)
1058
+ m.set(e.id, e);
1059
+ return m;
1060
+ }, [graph.edges]);
1061
+ const pathSegments = useMemo(() => {
1062
+ if (!path)
1063
+ return [];
1064
+ const nodes2 = path.nodeIds || [];
1065
+ const edges2 = path.edgeIds || [];
1066
+ const segs = [];
1067
+ for (let i = 0; i < Math.min(nodes2.length - 1, edges2.length); i++) {
1068
+ const from = nodes2[i];
1069
+ const to = nodes2[i + 1];
1070
+ const edgeId = edges2[i];
1071
+ const e = edgeById.get(edgeId);
1072
+ const predicate = String(e?.data?.predicateSummary || e?.label || '').trim();
1073
+ const dir = e && e.source === from && e.target === to ? 'forward' : e && e.source === to && e.target === from ? 'reverse' : 'unknown';
1074
+ segs.push({ from, to, edgeId, predicate, dir });
1075
+ }
1076
+ return segs;
1077
+ }, [edgeById, path]);
1078
+ const runQuery = useCallback(async () => {
1079
+ if (!onQuery)
1080
+ return;
1081
+ setQueryError('');
1082
+ setExpandError('');
1083
+ setQueryLoading(true);
1084
+ try {
1085
+ const minScoreValue = parseOptionalFloat(minScore);
1086
+ const limitValue = parseOptionalInt(limit, { allowZero: true, allowNegativeOne: true });
1087
+ const maxInputTokensValue = parseOptionalInt(maxInputTokens, { allowZero: true });
1088
+ const res = await onQuery({
1089
+ scope,
1090
+ owner_id: undefined,
1091
+ recall_level: recallLevel,
1092
+ query_text: queryText || undefined,
1093
+ subject: subject || undefined,
1094
+ predicate: predicate || undefined,
1095
+ object: object || undefined,
1096
+ min_score: minScoreValue,
1097
+ limit: limitValue,
1098
+ max_input_tokens: maxInputTokensValue,
1099
+ model: model || undefined,
1100
+ });
1101
+ setOverride(res);
1102
+ setOverrideKind('live query');
1103
+ if (queryMode2 === 'replace' && onItemsReplace && res && Array.isArray(res.items)) {
1104
+ onItemsReplace(res.items, { kind: 'live query', result: res });
1105
+ }
1106
+ }
1107
+ catch (e) {
1108
+ const msg = e instanceof Error ? e.message : String(e);
1109
+ setQueryError(msg || 'Query failed');
1110
+ }
1111
+ finally {
1112
+ setQueryLoading(false);
1113
+ }
1114
+ }, [limit, maxInputTokens, minScore, model, object, onItemsReplace, onQuery, predicate, queryMode2, queryText, recallLevel, scope, subject]);
1115
+ const expandNeighborhoodForPath = useCallback(async () => {
1116
+ if (!onQuery)
1117
+ return;
1118
+ const s = String(pathStart || '').trim();
1119
+ const e = String(pathEnd || '').trim();
1120
+ if (!s || !e)
1121
+ return;
1122
+ const fetchLimit = Math.max(200, parseOptionalInt(limit) ?? 80);
1123
+ const maxExpandNodes = 60;
1124
+ setExpandError('');
1125
+ setQueryError('');
1126
+ setExpandLoading(true);
1127
+ try {
1128
+ const merged = [];
1129
+ const seenAssertions = new Set();
1130
+ const addAssertion = (a) => {
1131
+ if (!a || typeof a !== 'object')
1132
+ return;
1133
+ if (typeof a.subject !== 'string' || typeof a.predicate !== 'string' || typeof a.object !== 'string')
1134
+ return;
1135
+ const key = `${a.subject}|${a.predicate}|${a.object}|${String(a.observed_at || '')}|${String(a.scope || '')}|${String(a.owner_id || '')}`;
1136
+ if (seenAssertions.has(key))
1137
+ return;
1138
+ seenAssertions.add(key);
1139
+ merged.push(a);
1140
+ };
1141
+ // Seed with the currently loaded subgraph.
1142
+ for (const a of displayItems)
1143
+ addAssertion(a);
1144
+ // Incremental neighborhood expansion (bounded) until we either find a path or hit caps.
1145
+ const directed = directedPath;
1146
+ const expanded = new Set();
1147
+ const frontier = [s];
1148
+ const discovered = new Set([s]);
1149
+ const expandNode = async (nodeId) => {
1150
+ // Outgoing (subject==node) always.
1151
+ const out = await onQuery({
1152
+ scope,
1153
+ recall_level: recallLevel,
1154
+ min_score: 0,
1155
+ limit: fetchLimit,
1156
+ subject: nodeId,
1157
+ query_text: undefined,
1158
+ max_input_tokens: 0,
1159
+ model: undefined,
1160
+ });
1161
+ if (out && Array.isArray(out.items)) {
1162
+ for (const it of out.items) {
1163
+ addAssertion(it);
1164
+ if (typeof it.subject === 'string' && it.subject.trim() === nodeId && typeof it.object === 'string') {
1165
+ const neigh = it.object.trim();
1166
+ if (neigh && !discovered.has(neigh)) {
1167
+ discovered.add(neigh);
1168
+ frontier.push(neigh);
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+ // In undirected mode, also include incoming (object==node).
1174
+ if (!directed) {
1175
+ const inc = await onQuery({
1176
+ scope,
1177
+ recall_level: recallLevel,
1178
+ min_score: 0,
1179
+ limit: fetchLimit,
1180
+ object: nodeId,
1181
+ query_text: undefined,
1182
+ max_input_tokens: 0,
1183
+ model: undefined,
1184
+ });
1185
+ if (inc && Array.isArray(inc.items)) {
1186
+ for (const it of inc.items) {
1187
+ addAssertion(it);
1188
+ if (typeof it.object === 'string' && it.object.trim() === nodeId && typeof it.subject === 'string') {
1189
+ const neigh = it.subject.trim();
1190
+ if (neigh && !discovered.has(neigh)) {
1191
+ discovered.add(neigh);
1192
+ frontier.push(neigh);
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+ }
1198
+ };
1199
+ while (frontier.length && expanded.size < maxExpandNodes) {
1200
+ if (discovered.has(e))
1201
+ break;
1202
+ const cur = frontier.shift();
1203
+ if (!cur || expanded.has(cur))
1204
+ continue;
1205
+ expanded.add(cur);
1206
+ await expandNode(cur);
1207
+ }
1208
+ setOverride({
1209
+ ok: true,
1210
+ count: merged.length,
1211
+ items: merged,
1212
+ active_memory_text: displayActiveMemoryText,
1213
+ raw: { expanded_for_path: true, expanded_nodes: expanded.size, discovered_nodes: discovered.size, merged: merged.length, directed },
1214
+ });
1215
+ setOverrideKind('expanded neighborhood');
1216
+ if (queryMode2 === 'replace' && onItemsReplace) {
1217
+ onItemsReplace(merged, {
1218
+ kind: 'expanded neighborhood',
1219
+ result: {
1220
+ ok: true,
1221
+ count: merged.length,
1222
+ items: merged,
1223
+ active_memory_text: displayActiveMemoryText,
1224
+ raw: { expanded_for_path: true, expanded_nodes: expanded.size, discovered_nodes: discovered.size, merged: merged.length, directed },
1225
+ },
1226
+ });
1227
+ }
1228
+ }
1229
+ catch (err) {
1230
+ const msg = err instanceof Error ? err.message : String(err);
1231
+ setExpandError(msg || 'Expand neighborhood failed');
1232
+ }
1233
+ finally {
1234
+ setExpandLoading(false);
1235
+ }
1236
+ }, [directedPath, displayActiveMemoryText, displayItems, limit, onItemsReplace, onQuery, pathEnd, pathStart, queryMode2, recallLevel, scope]);
1237
+ const resetToStep = useCallback(() => {
1238
+ setOverride(null);
1239
+ setOverrideKind(null);
1240
+ setQueryError('');
1241
+ setQueryLoading(false);
1242
+ setExpandError('');
1243
+ setExpandLoading(false);
1244
+ setPathStart('');
1245
+ setPathEnd('');
1246
+ setSelectedNodeId('');
1247
+ setSelectedEdgeId('');
1248
+ }, []);
1249
+ const header = title ? `KG snapshot (${title})` : 'KG snapshot';
1250
+ const nodeCount = graph.nodes.length;
1251
+ const edgeCount = graph.edges.length;
1252
+ const itemCount = visibleItems.length;
1253
+ return (_jsxs("div", { className: "amx-root", children: [_jsxs("div", { className: "amx-left", children: [_jsx("div", { className: "amx-graph", "aria-label": "Knowledge graph", children: _jsx(ReactFlowProvider, { children: _jsxs(ReactFlow, { nodes: nodes, edges: edges, defaultViewport: { x: 0, y: 0, zoom: 1 }, minZoom: AMX_VIEWPORT_MIN_ZOOM, maxZoom: AMX_VIEWPORT_MAX_ZOOM, onInit: (inst) => {
1254
+ flowRef.current = inst;
1255
+ const vp = pendingViewportRef.current;
1256
+ if (vp && typeof inst.setViewport === 'function') {
1257
+ try {
1258
+ inst.setViewport(vp, { duration: 0 });
1259
+ }
1260
+ catch {
1261
+ try {
1262
+ inst.setViewport(vp);
1263
+ }
1264
+ catch {
1265
+ // ignore
1266
+ }
1267
+ }
1268
+ }
1269
+ if (pendingFitViewRef.current && typeof inst.fitView === 'function') {
1270
+ try {
1271
+ inst.fitView({ padding: 0.2, duration: 0 });
1272
+ }
1273
+ catch {
1274
+ try {
1275
+ inst.fitView({ padding: 0.2 });
1276
+ }
1277
+ catch {
1278
+ // ignore
1279
+ }
1280
+ }
1281
+ }
1282
+ pendingViewportRef.current = null;
1283
+ pendingFitViewRef.current = false;
1284
+ }, nodesDraggable: true, nodesConnectable: false, elementsSelectable: true, onNodesChange: onNodesChange, onNodeClick: (_, n) => {
1285
+ setSelectedEdgeId('');
1286
+ setSelectedNodeId(n.id);
1287
+ }, onEdgeClick: (_, e) => {
1288
+ setSelectedNodeId('');
1289
+ setSelectedEdgeId(e.id);
1290
+ }, onPaneClick: () => {
1291
+ setSelectedNodeId('');
1292
+ setSelectedEdgeId('');
1293
+ }, children: [_jsx(Controls, {}), _jsx(Panel, { position: "top-right", children: _jsxs("div", { className: "amx-panel-toggles", children: [_jsx("button", { type: "button", className: "amx-minimap-toggle", onClick: () => setShowMiniMap((v) => !v), title: showMiniMap ? 'Hide minimap preview' : 'Show minimap preview', "aria-label": showMiniMap ? 'Hide minimap preview' : 'Show minimap preview', children: showMiniMap ? 'minimap on' : 'minimap off' }), layoutKind === 'force' ? (_jsx("button", { type: "button", className: "amx-minimap-toggle", onClick: toggleSimulation, title: layoutPlaying ? 'Pause force layout simulation' : 'Play force layout simulation', "aria-label": layoutPlaying ? 'Pause simulation' : 'Play simulation', children: layoutPlaying ? 'sim pause' : 'sim play' })) : null] }) }), showMiniMap ? (_jsx(MiniMap, { pannable: true, zoomable: true, maskColor: "rgba(0,0,0,0.45)", style: {
1294
+ background: 'rgba(12, 18, 34, 0.88)',
1295
+ border: '1px solid rgba(255,255,255,0.12)',
1296
+ borderRadius: 10,
1297
+ width: compactViewport ? 120 : 160,
1298
+ height: compactViewport ? 80 : 110,
1299
+ }, nodeColor: (n) => kindColor(n.data?.kind).minimap })) : null, _jsx(Background, { gap: 20, size: 1, color: "rgba(255,255,255,0.06)" })] }) }) }), _jsxs("details", { className: "amx-panel amx-controls", children: [_jsxs("summary", { children: [_jsx("span", { className: "amx-controls-title", children: header }), _jsxs("span", { className: "amx-small", children: [itemCount, " assertions \u00B7 ", nodeCount, " nodes \u00B7 ", edgeCount, " edges"] })] }), _jsxs("div", { className: "amx-toolbar", style: { marginTop: 10 }, children: [_jsxs("div", { className: "amx-toolbar-grid", children: [_jsxs("label", { children: ["search / highlight", _jsx("input", { value: search, onChange: (e) => setSearch(e.target.value), placeholder: "filter nodes by id/label" })] }), _jsxs("label", { children: ["group edges", _jsxs("select", { value: String(groupEdges), onChange: (e) => setGroupEdges(e.target.value === 'true'), children: [_jsx("option", { value: "true", children: "group (subject\u2192object)" }), _jsx("option", { value: "false", children: "no grouping" })] })] }), _jsxs("label", { children: ["view", _jsxs("select", { value: String(showStructural), onChange: (e) => setShowStructural(e.target.value === 'true'), children: [_jsx("option", { value: "true", children: "all assertions" }), _jsx("option", { value: "false", children: "hide rdf:type / labels" })] })] }), _jsxs("label", { children: ["path mode", _jsxs("select", { value: String(directedPath), onChange: (e) => setDirectedPath(e.target.value === 'true'), children: [_jsx("option", { value: "false", children: "undirected" }), _jsx("option", { value: "true", children: "directed" })] })] })] }), _jsxs("details", { className: "amx-details", children: [_jsx("summary", { children: "layout" }), _jsxs("div", { className: "amx-toolbar-grid", style: { marginTop: 10 }, children: [_jsxs("label", { children: ["layout", _jsxs("select", { value: layoutKind, onChange: (e) => setLayoutKind(normalizeLayoutKind(e.target.value, layoutKind)), children: [_jsx("option", { value: "grid", children: "grid (deterministic)" }), _jsx("option", { value: "radial", children: "radial (bfs)" }), _jsx("option", { value: "circle", children: "circle" }), _jsx("option", { value: "force", children: "force (simulation)" })] })] }), _jsxs("label", { children: ["seed", _jsx("input", { type: "number", step: "1", value: layoutSeed, onChange: (e) => setLayoutSeed(Math.trunc(Number(e.target.value || 0) || 0)) })] })] }), _jsxs("div", { className: "amx-actions", style: { marginTop: 10 }, children: [_jsx("button", { type: "button", className: "amx-btn", onClick: () => applyLayoutNow({ kind: layoutKind, seed: layoutSeed }), disabled: !nodeIds.length, children: "Apply layout" }), layoutKind === 'force' ? (_jsx("button", { type: "button", className: "amx-btn", onClick: toggleSimulation, disabled: !nodeIds.length, children: layoutPlaying ? 'Pause simulation' : 'Play simulation' })) : null, _jsx("button", { type: "button", className: "amx-btn", onClick: saveLayoutNow, disabled: !nodeIds.length, children: "Save layout" }), hasSavedLayout ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "amx-btn", onClick: loadSavedLayoutNow, children: "Load saved" }), _jsx("button", { type: "button", className: "amx-btn", onClick: clearSavedLayoutNow, children: "Clear saved" })] })) : null] }), _jsxs("div", { className: "amx-small", style: { marginTop: 10, opacity: 0.9 }, children: ["Drag nodes to adjust manually; use ", _jsx("span", { className: "amx-mono", children: "Save layout" }), " for a stable, replayable view.", hasSavedLayout ? (_jsxs("span", { children: [' ', "(saved", savedLayoutAt ? `: ${savedLayoutAt}` : '', ")"] })) : (_jsx("span", { children: " (no saved layout)" }))] })] }), _jsxs("details", { className: "amx-details", children: [_jsx("summary", { children: "path" }), _jsxs("div", { className: "amx-toolbar-grid", style: { marginTop: 10 }, children: [_jsxs("label", { children: ["path start", _jsxs("select", { value: pathStart, onChange: (e) => setPathStart(e.target.value), children: [_jsx("option", { value: "", children: "(none)" }), nodeIds.map((id) => (_jsx("option", { value: id, children: (() => {
1300
+ const label = nodeLabelById.get(id) || id;
1301
+ return label === id ? label : `${label} (${id})`;
1302
+ })() }, id)))] })] }), _jsxs("label", { children: ["path end", _jsxs("select", { value: pathEnd, onChange: (e) => setPathEnd(e.target.value), children: [_jsx("option", { value: "", children: "(none)" }), nodeIds.map((id) => (_jsx("option", { value: id, children: (() => {
1303
+ const label = nodeLabelById.get(id) || id;
1304
+ return label === id ? label : `${label} (${id})`;
1305
+ })() }, id)))] })] })] }), path ? (_jsx("div", { className: "amx-small", style: { marginTop: 10 }, children: pathSegments.map((seg, idx) => {
1306
+ const fromLabel = nodeLabelById.get(seg.from) || seg.from;
1307
+ const toLabel = nodeLabelById.get(seg.to) || seg.to;
1308
+ const arrow = seg.dir === 'reverse' ? ' ← ' : ' → ';
1309
+ const pred = seg.predicate ? ` (${seg.predicate})` : '';
1310
+ return (_jsxs("div", { children: [idx === 0 ? _jsx("span", { children: fromLabel }) : null, arrow, _jsxs("span", { children: [toLabel, pred] })] }, `${seg.edgeId}:${idx}`));
1311
+ }) })) : pathStart && pathEnd ? (_jsxs("div", { className: "amx-small", style: { marginTop: 10, opacity: 0.9 }, children: ["No path found in the current subgraph (", noPathDiagnostics?.assertions ?? itemCount, " assertions \u00B7 ", noPathDiagnostics?.nodes ?? graph.nodes.length, " nodes \u00B7", ' ', noPathDiagnostics?.edges ?? graph.edges.length, " edges). Start reaches ", noPathDiagnostics?.reachableFromStart ?? 0, " nodes.", onQuery ? (_jsx("div", { style: { marginTop: 10 }, children: _jsx("button", { type: "button", className: "amx-btn", onClick: () => void expandNeighborhoodForPath(), disabled: expandLoading, children: expandLoading ? 'Expanding…' : 'Expand neighborhood' }) })) : null] })) : (_jsx("div", { className: "amx-small", style: { marginTop: 10 }, children: "(no path)" }))] }), _jsxs("details", { className: "amx-details", children: [_jsx("summary", { children: "query" }), _jsxs("div", { className: "amx-toolbar-grid", style: { marginTop: 10 }, children: [_jsxs("label", { children: ["query_text (semantic)", _jsx("input", { value: queryText, onChange: (e) => setQueryText(e.target.value), placeholder: "e.g. emotion chip" })] }), _jsxs("label", { children: ["subject (exact)", _jsx("input", { value: subject, onChange: (e) => setSubject(e.target.value), placeholder: "e.g. ex:person-data" })] }), _jsxs("label", { children: ["predicate (exact)", _jsx("input", { value: predicate, onChange: (e) => setPredicate(e.target.value), placeholder: "e.g. schema:about" })] }), _jsxs("label", { children: ["object (exact)", _jsx("input", { value: object, onChange: (e) => setObject(e.target.value), placeholder: "e.g. ex:concept-emotion-chip" })] }), _jsxs("label", { children: ["min_score", _jsx("input", { type: "number", step: "any", value: minScore, onChange: (e) => setMinScore(e.target.value), placeholder: "(auto)" })] }), _jsxs("label", { children: ["limit", _jsx("input", { type: "number", min: "-1", step: "1", value: limit, onChange: (e) => setLimit(e.target.value), placeholder: "0/-1 = unlimited (if supported)" })] })] })] }), _jsxs("details", { className: "amx-details", children: [_jsx("summary", { children: "advanced" }), _jsxs("div", { className: "amx-toolbar-grid", style: { marginTop: 10 }, children: [_jsxs("label", { children: ["scope", _jsxs("select", { value: scope, onChange: (e) => setScope(normalizeScope(e.target.value, scope)), children: [_jsx("option", { value: "run", children: "run" }), _jsx("option", { value: "session", children: "session" }), _jsx("option", { value: "global", children: "global" }), _jsx("option", { value: "all", children: "all" })] })] }), _jsxs("label", { children: ["recall level", _jsxs("select", { value: recallLevel, onChange: (e) => setRecallLevel(e.target.value || 'standard'), children: [_jsx("option", { value: "urgent", children: "urgent" }), _jsx("option", { value: "standard", children: "standard" }), _jsx("option", { value: "deep", children: "deep" })] })] }), _jsxs("label", { children: ["max_input_tokens", _jsx("input", { type: "number", min: "0", step: "1", value: maxInputTokens, onChange: (e) => setMaxInputTokens(e.target.value), placeholder: "(auto)" })] }), _jsxs("label", { children: ["model (budgeting)", _jsx("input", { value: model, onChange: (e) => setModel(e.target.value), placeholder: "qwen/qwen3-next-80b" })] })] })] }), _jsxs("div", { className: "amx-actions", children: [_jsx("button", { type: "button", className: "amx-btn", onClick: () => void runQuery(), disabled: !onQuery || queryLoading, children: queryLoading ? 'Querying…' : 'Query store' }), _jsx("button", { type: "button", className: "amx-btn", onClick: resetToStep, disabled: !override && !queryError && !queryLoading, children: "Reset to step output" }), override ? (_jsxs("span", { className: "amx-small", children: ["showing: ", overrideKind || 'live query'] })) : (_jsx("span", { className: "amx-small", children: "showing: step output" })), queryError ? _jsx("span", { className: "amx-small", style: { color: 'rgba(255, 80, 80, 0.95)' }, children: queryError }) : null, expandError ? _jsx("span", { className: "amx-small", style: { color: 'rgba(255, 80, 80, 0.95)' }, children: expandError }) : null] }), _jsxs("details", { className: "amx-details", children: [_jsx("summary", { children: "node colors" }), _jsx("div", { className: "amx-legend", style: { marginTop: 10 }, children: [
1312
+ { k: 'person', label: 'person' },
1313
+ { k: 'org', label: 'org' },
1314
+ { k: 'concept', label: 'concept' },
1315
+ { k: 'claim', label: 'claim' },
1316
+ { k: 'event', label: 'event' },
1317
+ { k: 'doc', label: 'doc' },
1318
+ { k: 'vocab', label: 'vocab' },
1319
+ ].map(({ k, label }) => (_jsx("span", { className: "amx-legend-item", style: { borderColor: kindColor(k).stroke }, children: label }, k))) })] })] })] })] }), _jsxs("div", { className: "amx-right", children: [_jsxs("div", { className: "amx-panel", children: [_jsx("h3", { children: "Details" }), _jsx("div", { className: "amx-small", style: { marginBottom: 8 }, children: "Derived from `memory_kg_query` packetization (max_input_tokens); safe to inject into an LLM system prompt." }), (() => {
1320
+ const s = formatEffort(displayEffort);
1321
+ if (!s)
1322
+ return null;
1323
+ return (_jsx("div", { className: "amx-small", style: { marginBottom: 8, opacity: 0.9 }, children: s }));
1324
+ })(), (() => {
1325
+ const w = displayWarnings;
1326
+ if (!w)
1327
+ return null;
1328
+ const text = typeof w === 'string' ? w : Array.isArray(w) ? w.map((x) => String(x)).join(' · ') : JSON.stringify(w);
1329
+ if (!text || !text.trim())
1330
+ return null;
1331
+ return (_jsx("div", { className: "amx-small", style: { marginBottom: 8, color: 'rgba(255, 180, 90, 0.95)' }, children: text }));
1332
+ })(), _jsx("div", { className: "amx-small", style: { marginBottom: 8, opacity: 0.9 }, children: (() => {
1333
+ const pv = override?.packets_version ?? stepPacketsVersion;
1334
+ const pc = override?.packed_count ?? stepPackedCount;
1335
+ const dr = override?.dropped ?? stepDropped;
1336
+ const et = override?.estimated_tokens ?? stepEstimatedTokens;
1337
+ const parts = [];
1338
+ if (typeof pv === 'number')
1339
+ parts.push(`packets_v${pv}`);
1340
+ if (typeof pc === 'number')
1341
+ parts.push(`packed=${pc}`);
1342
+ if (typeof dr === 'number')
1343
+ parts.push(`dropped=${dr}`);
1344
+ if (typeof et === 'number')
1345
+ parts.push(`est_tokens=${et}`);
1346
+ return parts.length ? parts.join(' · ') : 'No packetization stats (set max_input_tokens > 0).';
1347
+ })() }), _jsx("div", { className: "amx-active-memory", children: renderHighlight(displayActiveMemoryText || '(empty)', search) }), (() => {
1348
+ const pkts = (override?.packets && Array.isArray(override.packets) ? override.packets : stepPackets);
1349
+ if (!Array.isArray(pkts) || pkts.length === 0)
1350
+ return null;
1351
+ return (_jsxs("div", { style: { marginTop: 10 }, children: [_jsxs("button", { type: "button", className: "amx-btn", onClick: () => setShowPackets((v) => !v), children: [showPackets ? 'Hide' : 'Show', " packets (", pkts.length, ")"] }), showPackets ? (_jsx("div", { className: "amx-list", style: { marginTop: 10 }, children: pkts.slice(0, 120).map((p, idx) => {
1352
+ const obj = p && typeof p === 'object' && !Array.isArray(p) ? p : null;
1353
+ const stmt = obj && typeof obj.statement === 'string' ? obj.statement : typeof p === 'string' ? p : '';
1354
+ const score = obj && (typeof obj.retrieval_score === 'number' || typeof obj.retrieval_score === 'string') ? obj.retrieval_score : null;
1355
+ const ts = obj && typeof obj.observed_at === 'string' ? obj.observed_at : null;
1356
+ return (_jsxs("div", { className: "amx-item", children: [_jsx("div", { className: "amx-mono", children: stmt || '(packet)' }), ts || score !== null ? (_jsxs("div", { className: "amx-small", children: [ts ? `[${ts}]` : null, ts && score !== null ? ' ' : null, score !== null ? `score:${score}` : null] })) : null] }, idx));
1357
+ }) })) : null] }));
1358
+ })(), _jsx("div", { style: { marginTop: 14, marginBottom: 12, borderTop: '1px solid rgba(255,255,255,0.08)' } }), _jsx("div", { className: "amx-small", style: { marginBottom: 8, fontWeight: 700, opacity: 0.9 }, children: "Inspect" }), _jsx("div", { className: "amx-small", style: { marginBottom: 8 }, children: "Click a node or edge to inspect its assertions." }), selectedNodeId ? (_jsxs("div", { className: "amx-item", style: { marginBottom: 10, display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center' }, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsx("div", { className: "amx-mono", style: { fontSize: 11, opacity: 0.95 }, children: nodeLabelById.get(selectedNodeId) || selectedNodeId }), _jsxs("div", { className: "amx-small", children: ["kind=", nodeKindById.get(selectedNodeId) || 'entity', " \u00B7 id=", selectedNodeId] })] }), _jsxs("div", { className: "amx-actions", children: [_jsx("button", { type: "button", className: "amx-btn", onClick: () => void copyText(selectedNodeId), children: "Copy id" }), _jsx("button", { type: "button", className: "amx-btn", onClick: fitSelected, children: "Focus" })] })] })) : selectedEdge && selectedEdge.source && selectedEdge.target ? (_jsxs("div", { className: "amx-item", style: { marginBottom: 10, display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center' }, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsxs("div", { className: "amx-mono", style: { fontSize: 11, opacity: 0.95 }, children: [selectedEdge.source, " \u2014", String(selectedEdge.label || selectedEdge.data?.predicateSummary || '').trim() || '?', "\u2192 ", selectedEdge.target] }), _jsxs("div", { className: "amx-small", children: ["edge_id=", selectedEdge.id] })] }), _jsxs("div", { className: "amx-actions", children: [_jsx("button", { type: "button", className: "amx-btn", onClick: () => void copyText(selectedEdge.id), children: "Copy id" }), _jsx("button", { type: "button", className: "amx-btn", onClick: fitSelected, children: "Fit" })] })] })) : (_jsx("div", { className: "amx-small", style: { marginBottom: 10, opacity: 0.9 }, children: "(no selection)" })), _jsx("div", { className: "amx-list", children: selectedAssertions.slice(0, 80).map((a, idx) => {
1359
+ const s = String(a.subject || '').trim();
1360
+ const p = String(a.predicate || '').trim();
1361
+ const o = String(a.object || '').trim();
1362
+ const sLabel = nodeLabelById.get(s) || s;
1363
+ const oLabel = nodeLabelById.get(o) || o;
1364
+ const sKind = nodeKindById.get(s) || 'entity';
1365
+ const oKind = nodeKindById.get(o) || 'entity';
1366
+ const sColor = kindColor(sKind);
1367
+ const oColor = kindColor(oKind);
1368
+ const t = formatUtcMinute(a.observed_at);
1369
+ const meta = formatAssertionMeta(a);
1370
+ const prov = a.provenance && typeof a.provenance === 'object' && !Array.isArray(a.provenance) ? a.provenance : null;
1371
+ const spanId = prov && typeof prov.span_id === 'string' ? prov.span_id.trim() : '';
1372
+ const writerRunId = prov && typeof prov.writer_run_id === 'string' ? prov.writer_run_id.trim() : '';
1373
+ const ownerId = typeof a.owner_id === 'string' ? a.owner_id.trim() : '';
1374
+ const spanRunId = writerRunId || ownerId;
1375
+ const canOpenSpan = Boolean((onOpenTranscript || onOpenSpan) && spanId && spanRunId);
1376
+ const canOpenRun = Boolean(onOpenTranscript && writerRunId && !spanId);
1377
+ const canOpen = canOpenSpan || canOpenRun;
1378
+ return (_jsxs("div", { className: "amx-item amx-triple-card", children: [_jsxs("div", { className: "amx-triple-row", children: [_jsx("button", { type: "button", className: "amx-term", style: { borderColor: sColor.stroke, background: sColor.bg }, title: s, onClick: () => {
1379
+ if (!s)
1380
+ return;
1381
+ setSelectedEdgeId('');
1382
+ setSelectedNodeId(s);
1383
+ }, children: sLabel || s || '(subject)' }), _jsx("span", { className: "amx-arrow", children: "\u2014" }), _jsx("span", { className: "amx-predicate", title: p, children: shortTerm(p) || '(predicate)' }), _jsx("span", { className: "amx-arrow", children: "\u2192" }), _jsx("button", { type: "button", className: "amx-term", style: { borderColor: oColor.stroke, background: oColor.bg }, title: o, onClick: () => {
1384
+ if (!o)
1385
+ return;
1386
+ setSelectedEdgeId('');
1387
+ setSelectedNodeId(o);
1388
+ }, children: oLabel || o || '(object)' })] }), _jsxs("div", { className: "amx-triple-meta", children: [t ? (_jsxs("span", { className: "amx-small", title: String(a.observed_at || ''), children: ["[", t, "]"] })) : null, meta ? _jsx("span", { className: "amx-small", children: meta }) : null] }), spanId || writerRunId ? (_jsxs("div", { className: "amx-triple-actions", children: [spanId ? _jsxs("span", { className: "amx-small", children: ["span:", spanId] }) : null, writerRunId ? _jsxs("span", { className: "amx-small", children: ["run:", writerRunId] }) : null, canOpen ? (_jsx("button", { type: "button", className: "amx-btn", onClick: () => {
1389
+ if (canOpenSpan && spanId && spanRunId) {
1390
+ openTranscript({ run_id: spanRunId, span_id: spanId, assertion: a });
1391
+ return;
1392
+ }
1393
+ if (canOpenRun && writerRunId) {
1394
+ openTranscript({ run_id: writerRunId, assertion: a });
1395
+ }
1396
+ }, children: "Open transcript" })) : null] })) : null] }, idx));
1397
+ }) })] }), _jsxs("div", { className: "amx-panel", children: [_jsx("h3", { children: "Transcript" }), _jsx("div", { className: "amx-small", style: { marginBottom: 8 }, children: "Pivot from a triple to its provenance transcript (span/note artifact, or run input fallback)." }), selectedSources.length ? (_jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("div", { className: "amx-small", style: { marginBottom: 6, opacity: 0.85 }, children: "spans" }), _jsx("div", { className: "amx-list", style: { maxHeight: 320 }, children: selectedSources.slice(0, 60).map((src) => (_jsxs("div", { className: "amx-item", style: { display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center' }, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsxs("div", { className: "amx-mono", style: { fontSize: 11, opacity: 0.95 }, children: ["span:", src.span_id] }), _jsxs("div", { className: "amx-small", children: ["run_id=", src.span_run_id, " \u00B7 assertions=", src.count, src.last_observed_at_fmt ? ` · last=[${src.last_observed_at_fmt}]` : ''] })] }), onOpenSpan || onOpenTranscript ? (_jsx("button", { type: "button", className: "amx-btn", onClick: () => openTranscript({ run_id: src.span_run_id, span_id: src.span_id, assertion: src.assertion }), children: "Open transcript" })) : null] }, `${src.span_run_id}|${src.span_id}`))) })] })) : null, onOpenTranscript && selectedRunTranscripts.length ? (_jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("div", { className: "amx-small", style: { marginBottom: 6, opacity: 0.85 }, children: "runs (fallback)" }), _jsx("div", { className: "amx-list", style: { maxHeight: 240 }, children: selectedRunTranscripts.slice(0, 40).map((src) => (_jsxs("div", { className: "amx-item", style: { display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center' }, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsxs("div", { className: "amx-mono", style: { fontSize: 11, opacity: 0.95 }, children: ["run:", src.run_id] }), _jsxs("div", { className: "amx-small", children: ["assertions=", src.count, src.last_observed_at_fmt ? ` · last=[${src.last_observed_at_fmt}]` : ''] })] }), _jsx("button", { type: "button", className: "amx-btn", onClick: () => openTranscript({ run_id: src.run_id, assertion: src.assertion }), children: "Open transcript" })] }, src.run_id))) })] })) : null, !selectedSources.length && !(onOpenTranscript && selectedRunTranscripts.length) ? _jsx("div", { className: "amx-small", children: "(no provenance)" }) : null] })] })] }));
1398
+ }