@petrarca/sonnet-graph 0.1.6

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/dist/index.js ADDED
@@ -0,0 +1,4074 @@
1
+ // src/models/visual-graph.ts
2
+ function isVisualNode(obj) {
3
+ return typeof obj === "object" && obj !== null && "id_" in obj && "label_" in obj && "properties_" in obj && "visual" in obj && typeof obj.visual === "object" && "displayName" in obj.visual && "color" in obj.visual;
4
+ }
5
+ function isVisualEdge(obj) {
6
+ return typeof obj === "object" && obj !== null && "id_" in obj && "label_" in obj && "start_id_" in obj && "end_id_" in obj && "visual" in obj && typeof obj.visual === "object" && "color" in obj.visual;
7
+ }
8
+
9
+ // src/models/graph-types.ts
10
+ function isRawGraphNode(obj) {
11
+ return "label_" in obj && !("start_id_" in obj) && !("end_id_" in obj);
12
+ }
13
+
14
+ // src/graph/extractDisplayLabel.ts
15
+ function interpolateTemplate(template, properties) {
16
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
17
+ const val = properties[key];
18
+ return val !== void 0 && val !== null ? String(val) : "";
19
+ }).replace(/\s+/g, " ").trim();
20
+ }
21
+ function resolveLabel(strategies, fallback) {
22
+ for (const strategy of strategies) {
23
+ const result = strategy();
24
+ if (result) return result;
25
+ }
26
+ return fallback;
27
+ }
28
+ function fromTemplate(displayConfig, props) {
29
+ if (!displayConfig?.labelTemplate || !props) return void 0;
30
+ return interpolateTemplate(displayConfig.labelTemplate, props) || void 0;
31
+ }
32
+ function fromLabelProperty(displayConfig, props) {
33
+ if (!displayConfig?.labelProperty || !props?.[displayConfig.labelProperty])
34
+ return void 0;
35
+ return String(props[displayConfig.labelProperty]);
36
+ }
37
+ function fromFallbackProperty(displayConfig, props) {
38
+ if (!displayConfig?.fallbackProperty || !props?.[displayConfig.fallbackProperty])
39
+ return void 0;
40
+ return String(props[displayConfig.fallbackProperty]);
41
+ }
42
+ function extractNodeDisplayLabel(node, displayConfig) {
43
+ const props = node?.properties_;
44
+ return resolveLabel(
45
+ [
46
+ () => fromTemplate(displayConfig, props),
47
+ () => fromLabelProperty(displayConfig, props),
48
+ () => fromFallbackProperty(displayConfig, props),
49
+ () => props?.name ? String(props.name) : void 0,
50
+ () => {
51
+ const keys = props ? Object.keys(props) : [];
52
+ return keys.length > 0 ? String(props[keys[0]]) : void 0;
53
+ },
54
+ () => node?.id_ !== void 0 && node.id_ !== null ? `Node #${node.id_}` : void 0
55
+ ],
56
+ "Unknown"
57
+ );
58
+ }
59
+ function extractEdgeDisplayLabel(edge, displayConfig) {
60
+ const props = edge?.properties_;
61
+ return resolveLabel(
62
+ [
63
+ () => fromTemplate(displayConfig, props),
64
+ () => fromLabelProperty(displayConfig, props),
65
+ () => fromFallbackProperty(displayConfig, props),
66
+ () => edge?.label_ ? edge.label_ : void 0,
67
+ () => edge?.id_ !== void 0 && edge.id_ !== null ? `Edge #${edge.id_}` : void 0
68
+ ],
69
+ "Unknown"
70
+ );
71
+ }
72
+ function extractDisplayLabel(item, displayConfig) {
73
+ if (isRawGraphNode(item)) {
74
+ return extractNodeDisplayLabel(item, displayConfig);
75
+ } else {
76
+ return extractEdgeDisplayLabel(item, displayConfig);
77
+ }
78
+ }
79
+
80
+ // src/graph/enrichment.ts
81
+ var BASE_COLORS = [
82
+ "#f94144",
83
+ "#118ab2",
84
+ "#f8961e",
85
+ "#43aa8b",
86
+ "#f3722c",
87
+ "#90be6d",
88
+ "#f9c74f",
89
+ "#c4bbaf"
90
+ ];
91
+ function buildColorPalette(labelCounts) {
92
+ const palette = {};
93
+ let i = 0;
94
+ for (const label of Object.keys(labelCounts)) {
95
+ palette[label] = BASE_COLORS[i % BASE_COLORS.length];
96
+ i++;
97
+ }
98
+ palette["neutral"] = "#6c757d";
99
+ palette["faded"] = "#adb5bd";
100
+ return palette;
101
+ }
102
+ function buildColorPaletteFromNodes(nodes) {
103
+ const labelCounts = {};
104
+ for (const n of nodes) {
105
+ const l = n?.label_;
106
+ if (l) labelCounts[l] = (labelCounts[l] || 0) + 1;
107
+ }
108
+ return buildColorPalette(labelCounts);
109
+ }
110
+ function enrichNode(apiNode, colorPalette, displayConfig) {
111
+ const displayName = extractDisplayLabel(apiNode, displayConfig);
112
+ const color = colorPalette[apiNode.label_];
113
+ return {
114
+ // Copy core data from API
115
+ id_: apiNode.id_ ?? 0,
116
+ // Handle optional id for creation scenarios
117
+ label_: apiNode.label_,
118
+ properties_: apiNode.properties_ ?? {},
119
+ // Add visual enrichment
120
+ visual: {
121
+ displayName,
122
+ // Use color from palette, or fall back to neutral if not found
123
+ // This ensures every node has a color (no undefined)
124
+ color: color || colorPalette["neutral"] || "#6c757d",
125
+ // Size can be added later if needed
126
+ size: void 0,
127
+ icon: void 0
128
+ }
129
+ };
130
+ }
131
+ function enrichEdge(apiEdge, colorPalette) {
132
+ return {
133
+ // Copy core data from API
134
+ id_: apiEdge.id_ ?? 0,
135
+ label_: apiEdge.label_,
136
+ start_id_: apiEdge.start_id_,
137
+ end_id_: apiEdge.end_id_,
138
+ properties_: apiEdge.properties_,
139
+ // Add visual enrichment
140
+ visual: {
141
+ // Use neutral color from palette if available, otherwise default
142
+ color: colorPalette?.["neutral"] ?? "#999999",
143
+ // Default size of 3
144
+ size: 3,
145
+ dashed: void 0
146
+ }
147
+ };
148
+ }
149
+ function enrichNodes(apiNodes, colorPalette, labelDisplayMap) {
150
+ return apiNodes.map(
151
+ (node) => enrichNode(node, colorPalette, labelDisplayMap?.[node.label_])
152
+ );
153
+ }
154
+ function enrichEdges(apiEdges, colorPalette) {
155
+ return apiEdges.map((edge) => enrichEdge(edge, colorPalette));
156
+ }
157
+
158
+ // src/graph/normalize.ts
159
+ function normalizeGraph(nodes, edges) {
160
+ const nodesById = {};
161
+ const edgesById = {};
162
+ for (const n of nodes) {
163
+ const key = String(n.id_);
164
+ nodesById[key] = n;
165
+ }
166
+ for (const e of edges) {
167
+ const key = String(e.id_);
168
+ edgesById[key] = e;
169
+ }
170
+ return {
171
+ nodesById,
172
+ edgesById,
173
+ nodeIds: Object.keys(nodesById),
174
+ edgeIds: Object.keys(edgesById)
175
+ };
176
+ }
177
+ var EMPTY_NORMALIZED = {
178
+ nodesById: {},
179
+ edgesById: {},
180
+ nodeIds: [],
181
+ edgeIds: []
182
+ };
183
+
184
+ // src/stores/Selection.ts
185
+ import { create } from "zustand";
186
+ import { devLog } from "@petrarca/sonnet-core";
187
+ function createSelectionStore() {
188
+ return create()((set, get) => ({
189
+ selectedNodeIds: [],
190
+ setSelectedNodeIds: (ids) => {
191
+ const prev = get().selectedNodeIds;
192
+ if (prev.length === ids.length && prev.every((v, i) => v === ids[i])) {
193
+ return;
194
+ }
195
+ devLog("[SelectionStore] setSelectedNodeIds", { prev, next: ids });
196
+ set({ selectedNodeIds: ids });
197
+ },
198
+ selectedEdge: null,
199
+ setSelectedEdge: (edge) => {
200
+ devLog("[SelectionStore] setSelectedEdge", {
201
+ prev: get().selectedEdge?.id_,
202
+ next: edge?.id_
203
+ });
204
+ set({ selectedEdge: edge });
205
+ },
206
+ clear: () => {
207
+ devLog("[SelectionStore] clear called");
208
+ set({ selectedNodeIds: [], selectedEdge: null });
209
+ }
210
+ }));
211
+ }
212
+ var useSelection = createSelectionStore();
213
+
214
+ // src/stores/GraphData.ts
215
+ import { create as create2 } from "zustand";
216
+ import { devLog as devLog2 } from "@petrarca/sonnet-core";
217
+ function createGraphDataStore() {
218
+ return create2()((set, get) => ({
219
+ nodes: [],
220
+ edges: [],
221
+ stats: void 0,
222
+ values: [],
223
+ isValueSet: false,
224
+ queryNodeIds: /* @__PURE__ */ new Set(),
225
+ expandedNodeIds: /* @__PURE__ */ new Set(),
226
+ queryEdgeIds: /* @__PURE__ */ new Set(),
227
+ expandedEdgeIds: /* @__PURE__ */ new Set(),
228
+ setQueryData: (nodes, edges, stats, values, isValueSet = false) => {
229
+ const nodeIds = new Set(nodes.map((n) => String(n.id_)));
230
+ const edgeIds = new Set(edges.map((e) => String(e.id_)));
231
+ devLog2("[GraphData] setQueryData", {
232
+ nodes: nodes.length,
233
+ edges: edges.length
234
+ });
235
+ set({
236
+ nodes,
237
+ edges,
238
+ stats,
239
+ values: values || [],
240
+ isValueSet,
241
+ queryNodeIds: nodeIds,
242
+ queryEdgeIds: edgeIds,
243
+ expandedNodeIds: /* @__PURE__ */ new Set(),
244
+ expandedEdgeIds: /* @__PURE__ */ new Set()
245
+ });
246
+ },
247
+ mergeExpandedData: (newNodes, newEdges) => {
248
+ const state = get();
249
+ const existingNodesMap = new Map(
250
+ state.nodes.map((n) => [String(n.id_), n])
251
+ );
252
+ const existingEdgesMap = new Map(
253
+ state.edges.map((e) => [String(e.id_), e])
254
+ );
255
+ const newExpandedNodeIds = new Set(state.expandedNodeIds);
256
+ const newExpandedEdgeIds = new Set(state.expandedEdgeIds);
257
+ const nodesToAdd = [];
258
+ for (const node of newNodes) {
259
+ const id = String(node.id_);
260
+ if (!existingNodesMap.has(id)) {
261
+ nodesToAdd.push(node);
262
+ newExpandedNodeIds.add(id);
263
+ }
264
+ }
265
+ const edgesToAdd = [];
266
+ for (const edge of newEdges) {
267
+ const id = String(edge.id_);
268
+ if (!existingEdgesMap.has(id)) {
269
+ edgesToAdd.push(edge);
270
+ newExpandedEdgeIds.add(id);
271
+ }
272
+ }
273
+ devLog2("[GraphData] mergeExpandedData", {
274
+ newNodes: newNodes.length,
275
+ newEdges: newEdges.length,
276
+ addedNodes: nodesToAdd.length,
277
+ addedEdges: edgesToAdd.length,
278
+ duplicatesSkipped: {
279
+ nodes: newNodes.length - nodesToAdd.length,
280
+ edges: newEdges.length - edgesToAdd.length
281
+ }
282
+ });
283
+ if (nodesToAdd.length > 0 || edgesToAdd.length > 0) {
284
+ set({
285
+ nodes: [...state.nodes, ...nodesToAdd],
286
+ edges: [...state.edges, ...edgesToAdd],
287
+ expandedNodeIds: newExpandedNodeIds,
288
+ expandedEdgeIds: newExpandedEdgeIds
289
+ });
290
+ } else {
291
+ devLog2(
292
+ "[GraphData] mergeExpandedData: No new elements to add (all duplicates)"
293
+ );
294
+ }
295
+ },
296
+ clear: () => {
297
+ devLog2("[GraphData] clear");
298
+ set({
299
+ nodes: [],
300
+ edges: [],
301
+ stats: void 0,
302
+ values: [],
303
+ isValueSet: false,
304
+ queryNodeIds: /* @__PURE__ */ new Set(),
305
+ expandedNodeIds: /* @__PURE__ */ new Set(),
306
+ queryEdgeIds: /* @__PURE__ */ new Set(),
307
+ expandedEdgeIds: /* @__PURE__ */ new Set()
308
+ });
309
+ },
310
+ getStats: () => {
311
+ const state = get();
312
+ return {
313
+ totalNodes: state.nodes.length,
314
+ totalEdges: state.edges.length,
315
+ queryNodes: state.queryNodeIds.size,
316
+ expandedNodes: state.expandedNodeIds.size,
317
+ queryEdges: state.queryEdgeIds.size,
318
+ expandedEdges: state.expandedEdgeIds.size
319
+ };
320
+ }
321
+ }));
322
+ }
323
+ var useGraphData = createGraphDataStore();
324
+
325
+ // src/stores/GraphFilter.ts
326
+ import { create as create3 } from "zustand";
327
+ function calculateNodeLabelUpdate(affectedLabels, currentNodeFilters, allNodesDisabled) {
328
+ if (affectedLabels.length === 0) return null;
329
+ if (allNodesDisabled) {
330
+ return { allLabelsDisabled: false, nodeLabelFilters: affectedLabels };
331
+ }
332
+ if (currentNodeFilters.length > 0) {
333
+ const labelsToAdd = affectedLabels.filter(
334
+ (l) => !currentNodeFilters.includes(l)
335
+ );
336
+ if (labelsToAdd.length > 0) {
337
+ return { nodeLabelFilters: [...currentNodeFilters, ...labelsToAdd] };
338
+ }
339
+ }
340
+ return null;
341
+ }
342
+ function handleEnableFromAllDisabled(label, getNodeLabelUpdate, set) {
343
+ const nodeUpdate = getNodeLabelUpdate(label);
344
+ set({
345
+ allEdgeLabelsDisabled: false,
346
+ edgeLabelFilters: [label],
347
+ ...nodeUpdate
348
+ });
349
+ }
350
+ function handleFirstFilter(label, allAvailableLabels, set) {
351
+ const next = allAvailableLabels.filter((l) => l !== label);
352
+ set({ edgeLabelFilters: next });
353
+ }
354
+ function handleToggleFilter(label, current, exists, getNodeLabelUpdate, set) {
355
+ const next = exists ? current.filter((l) => l !== label) : [...current, label];
356
+ if (next.length === 0 && exists) {
357
+ set({ allEdgeLabelsDisabled: true, edgeLabelFilters: [] });
358
+ return;
359
+ }
360
+ const nodeUpdate = exists ? null : getNodeLabelUpdate(label);
361
+ set({ edgeLabelFilters: next, ...nodeUpdate });
362
+ }
363
+ function createGraphFilterStore() {
364
+ return create3()((set, get) => ({
365
+ nodeLabelFilters: [],
366
+ allLabelsDisabled: false,
367
+ edgeLabelFilters: [],
368
+ allEdgeLabelsDisabled: false,
369
+ lastDataChangeTime: 0,
370
+ notifyDataChange: () => set({ lastDataChangeTime: Date.now() }),
371
+ resetFilters: () => set({
372
+ nodeLabelFilters: [],
373
+ allLabelsDisabled: false,
374
+ edgeLabelFilters: [],
375
+ allEdgeLabelsDisabled: false
376
+ }),
377
+ toggleNodeLabelFilter: (label, allAvailableLabels) => {
378
+ const wasAllDisabled = get().allLabelsDisabled;
379
+ if (wasAllDisabled) set({ allLabelsDisabled: false });
380
+ const current = get().nodeLabelFilters;
381
+ const exists = current.includes(label);
382
+ if (wasAllDisabled) {
383
+ set({ nodeLabelFilters: [label] });
384
+ } else if (current.length === 0 && !exists && allAvailableLabels && allAvailableLabels.length > 0) {
385
+ const next = allAvailableLabels.filter((l) => l !== label);
386
+ set({ nodeLabelFilters: next });
387
+ } else {
388
+ const next = exists ? current.filter((l) => l !== label) : [...current, label];
389
+ if (next.length === 0 && exists) {
390
+ set({ allLabelsDisabled: true, nodeLabelFilters: [] });
391
+ } else {
392
+ set({ nodeLabelFilters: next });
393
+ }
394
+ }
395
+ },
396
+ clearNodeLabelFilters: () => set({ nodeLabelFilters: [] }),
397
+ setNodeLabelFilters: (labels) => set({ nodeLabelFilters: labels }),
398
+ toggleAllLabelsDisabled: () => {
399
+ const now = !get().allLabelsDisabled;
400
+ set({ allLabelsDisabled: now, nodeLabelFilters: [] });
401
+ },
402
+ toggleEdgeLabelFilter: (label, allAvailableLabels, edgeToNodeLabels) => {
403
+ const getNodeLabelUpdate = (edgeLabel) => {
404
+ if (!edgeToNodeLabels) return null;
405
+ const affectedLabels = edgeToNodeLabels[edgeLabel] || [];
406
+ return calculateNodeLabelUpdate(
407
+ affectedLabels,
408
+ get().nodeLabelFilters,
409
+ get().allLabelsDisabled
410
+ );
411
+ };
412
+ if (get().allEdgeLabelsDisabled) {
413
+ handleEnableFromAllDisabled(label, getNodeLabelUpdate, set);
414
+ } else if (get().edgeLabelFilters.length === 0 && allAvailableLabels && allAvailableLabels.length > 0) {
415
+ handleFirstFilter(label, allAvailableLabels, set);
416
+ } else {
417
+ const current = get().edgeLabelFilters;
418
+ handleToggleFilter(
419
+ label,
420
+ current,
421
+ current.includes(label),
422
+ getNodeLabelUpdate,
423
+ set
424
+ );
425
+ }
426
+ },
427
+ clearEdgeLabelFilters: () => set({ edgeLabelFilters: [] }),
428
+ setEdgeLabelFilters: (labels) => set({ edgeLabelFilters: labels }),
429
+ toggleAllEdgeLabelsDisabled: () => {
430
+ const now = !get().allEdgeLabelsDisabled;
431
+ set({ allEdgeLabelsDisabled: now, edgeLabelFilters: [] });
432
+ }
433
+ }));
434
+ }
435
+ var useGraphFilter = createGraphFilterStore();
436
+
437
+ // src/stores/GraphExploration.ts
438
+ import { create as create4 } from "zustand";
439
+ import { devLog as devLog3 } from "@petrarca/sonnet-core";
440
+ function collectReachable(startId, direction, allEdges, includeStart) {
441
+ const nodes = /* @__PURE__ */ new Set();
442
+ const edges = /* @__PURE__ */ new Set();
443
+ const visited = /* @__PURE__ */ new Set();
444
+ const traverse = (currentId, isStart) => {
445
+ if (visited.has(currentId)) return;
446
+ visited.add(currentId);
447
+ if (!isStart || includeStart) nodes.add(currentId);
448
+ for (const edge of allEdges) {
449
+ const srcId = String(edge.start_id_);
450
+ const tgtId = String(edge.end_id_);
451
+ const edgeId = String(edge.id_);
452
+ if (direction === "in" && tgtId === currentId) {
453
+ edges.add(edgeId);
454
+ traverse(srcId, false);
455
+ } else if (direction === "out" && srcId === currentId) {
456
+ edges.add(edgeId);
457
+ traverse(tgtId, false);
458
+ }
459
+ }
460
+ };
461
+ traverse(startId, true);
462
+ return { nodes, edges };
463
+ }
464
+ function determineEdgeDirection(endpoints, selectedNodeIds) {
465
+ const { startNodeId, endNodeId } = endpoints;
466
+ if (selectedNodeIds.length !== 1) {
467
+ return { direction: "outgoing", referenceNodeId: startNodeId };
468
+ }
469
+ const selected = selectedNodeIds[0];
470
+ if (selected === startNodeId) {
471
+ return { direction: "outgoing", referenceNodeId: selected };
472
+ }
473
+ if (selected === endNodeId) {
474
+ return { direction: "incoming", referenceNodeId: selected };
475
+ }
476
+ devLog3(
477
+ "[GraphExploration] focusEdge - selected node not an endpoint, defaulting to outgoing",
478
+ { selectedNode: selected, startNodeId, endNodeId }
479
+ );
480
+ return { direction: "outgoing", referenceNodeId: startNodeId };
481
+ }
482
+ function buildFocusSet(referenceNodeId, direction, edgeLabel, allEdges) {
483
+ const focusNodeIds = /* @__PURE__ */ new Set([referenceNodeId]);
484
+ for (const e of allEdges) {
485
+ if (e.label_ !== edgeLabel) continue;
486
+ const sourceId = String(e.start_id_);
487
+ const targetId = String(e.end_id_);
488
+ if (direction === "outgoing" && sourceId === referenceNodeId) {
489
+ focusNodeIds.add(targetId);
490
+ } else if (direction === "incoming" && targetId === referenceNodeId) {
491
+ focusNodeIds.add(sourceId);
492
+ }
493
+ }
494
+ return focusNodeIds;
495
+ }
496
+ function handleExistingNode(nodeId, nodeIdStr, existingNodes, autoFocus, graphFocusStore) {
497
+ const existingNode = existingNodes.find((n) => n.id_ === nodeId);
498
+ if (!existingNode) return false;
499
+ if (autoFocus) {
500
+ devLog3("[GraphExploration] Node already in graph, focusing", { nodeId });
501
+ graphFocusStore.getState().requestFocus(nodeIdStr, "node", false);
502
+ } else {
503
+ devLog3("[GraphExploration] Node already in graph, skipping focus", {
504
+ nodeId
505
+ });
506
+ }
507
+ return true;
508
+ }
509
+ async function fetchAndEnrichNode(nodeId, existingNodes, stores) {
510
+ devLog3("[GraphExploration] Fetching node from backend", { nodeId });
511
+ const result = await stores.getNodes([nodeId]);
512
+ if (result.length === 0) {
513
+ throw new Error(`Node ${nodeId} not found`);
514
+ }
515
+ const colorPalette = buildColorPaletteFromNodes([
516
+ ...existingNodes,
517
+ ...result
518
+ ]);
519
+ const enrichedNodes = enrichNodes(
520
+ result,
521
+ colorPalette,
522
+ stores.getLabelDisplayMap?.()
523
+ );
524
+ devLog3("[GraphExploration] Enriched nodes", {
525
+ nodeId,
526
+ enrichedCount: enrichedNodes.length
527
+ });
528
+ stores.graphData.getState().mergeExpandedData(enrichedNodes, []);
529
+ return enrichedNodes;
530
+ }
531
+ function ensureNodeLabelInFilters(enrichedNodes, nodeId, graphFilterStore) {
532
+ const addedNode = enrichedNodes[0];
533
+ if (!addedNode) return;
534
+ const { nodeLabelFilters: currentFilters } = graphFilterStore.getState();
535
+ const nodeLabel = addedNode.label_;
536
+ if (currentFilters.length > 0 && !currentFilters.includes(nodeLabel)) {
537
+ devLog3("[GraphExploration] Adding node label to active filters", {
538
+ nodeId,
539
+ nodeLabel,
540
+ currentFilters
541
+ });
542
+ graphFilterStore.getState().setNodeLabelFilters([...currentFilters, nodeLabel]);
543
+ }
544
+ }
545
+ function createGraphExplorationStore(deps) {
546
+ const stores = deps;
547
+ return create4()((set, get) => ({
548
+ focusedNodeIds: /* @__PURE__ */ new Set(),
549
+ isFocusMode: false,
550
+ hiddenElementIds: /* @__PURE__ */ new Set(),
551
+ expandedNodeIds: /* @__PURE__ */ new Set(),
552
+ setFocusMode: (nodeIds) => {
553
+ devLog3("[GraphExploration] setFocusMode", {
554
+ nodeIds,
555
+ count: nodeIds.length
556
+ });
557
+ set({
558
+ focusedNodeIds: new Set(nodeIds),
559
+ isFocusMode: true
560
+ });
561
+ },
562
+ setFocusModeWithNeighborhood: (nodeId) => {
563
+ const edges = stores.graphData.getState().edges;
564
+ const connectedNodeIds = /* @__PURE__ */ new Set();
565
+ const nodeIdStr = String(nodeId);
566
+ for (const edge of edges) {
567
+ const sourceId = String(edge.start_id_);
568
+ const targetId = String(edge.end_id_);
569
+ if (sourceId === nodeIdStr) {
570
+ connectedNodeIds.add(targetId);
571
+ } else if (targetId === nodeIdStr) {
572
+ connectedNodeIds.add(sourceId);
573
+ }
574
+ }
575
+ const focusNodeIds = [nodeIdStr, ...Array.from(connectedNodeIds)];
576
+ devLog3("[GraphExploration] setFocusModeWithNeighborhood", {
577
+ nodeId: nodeIdStr,
578
+ connectedNodes: connectedNodeIds.size,
579
+ totalFocused: focusNodeIds.length
580
+ });
581
+ set({
582
+ focusedNodeIds: new Set(focusNodeIds),
583
+ isFocusMode: true
584
+ });
585
+ },
586
+ clearFocusMode: () => {
587
+ devLog3("[GraphExploration] clearFocusMode");
588
+ set({
589
+ focusedNodeIds: /* @__PURE__ */ new Set(),
590
+ isFocusMode: false
591
+ });
592
+ },
593
+ hideElements: (elementIds) => {
594
+ const current = get().hiddenElementIds;
595
+ const updated = new Set(current);
596
+ elementIds.forEach((id) => updated.add(id));
597
+ devLog3("[GraphExploration] hideElements", {
598
+ newIds: elementIds,
599
+ totalHidden: updated.size
600
+ });
601
+ set({ hiddenElementIds: updated });
602
+ },
603
+ showElements: (elementIds) => {
604
+ const current = get().hiddenElementIds;
605
+ const updated = new Set(current);
606
+ elementIds.forEach((id) => updated.delete(id));
607
+ devLog3("[GraphExploration] showElements", {
608
+ restoredIds: elementIds,
609
+ remainingHidden: updated.size
610
+ });
611
+ set({ hiddenElementIds: updated });
612
+ },
613
+ hideNodeIncoming: (nodeId) => {
614
+ devLog3("[GraphExploration] hideNodeIncoming - hiding predecessors", {
615
+ nodeId
616
+ });
617
+ const { edges } = stores.graphData.getState();
618
+ const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
619
+ nodeId,
620
+ "in",
621
+ edges,
622
+ false
623
+ );
624
+ devLog3("[GraphExploration] hideNodeIncoming - traversal complete", {
625
+ nodeId,
626
+ nodesToHide: nodesToHide.size,
627
+ edgesToHide: edgesToHide.size
628
+ });
629
+ const allElementsToHide = [...nodesToHide, ...edgesToHide];
630
+ if (allElementsToHide.length > 0) {
631
+ get().hideElements(allElementsToHide);
632
+ } else {
633
+ devLog3("[GraphExploration] hideNodeIncoming - no predecessors found");
634
+ }
635
+ },
636
+ hideNodeOutgoing: (nodeId) => {
637
+ devLog3("[GraphExploration] hideNodeOutgoing - hiding successors", {
638
+ nodeId
639
+ });
640
+ const { edges } = stores.graphData.getState();
641
+ const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
642
+ nodeId,
643
+ "out",
644
+ edges,
645
+ false
646
+ );
647
+ devLog3("[GraphExploration] hideNodeOutgoing - traversal complete", {
648
+ nodeId,
649
+ nodesToHide: nodesToHide.size,
650
+ edgesToHide: edgesToHide.size
651
+ });
652
+ const allElementsToHide = [...nodesToHide, ...edgesToHide];
653
+ if (allElementsToHide.length > 0) {
654
+ get().hideElements(allElementsToHide);
655
+ } else {
656
+ devLog3("[GraphExploration] hideNodeOutgoing - no successors found");
657
+ }
658
+ },
659
+ hideEdgeForwardPath: (edgeId) => {
660
+ devLog3(
661
+ "[GraphExploration] hideEdgeForwardPath - hiding edge + target + downstream",
662
+ { edgeId }
663
+ );
664
+ const { edges } = stores.graphData.getState();
665
+ const edge = edges.find((e) => String(e.id_) === edgeId);
666
+ if (!edge) {
667
+ devLog3("[GraphExploration] hideEdgeForwardPath - edge not found", {
668
+ edgeId
669
+ });
670
+ return;
671
+ }
672
+ const targetNodeId = String(edge.end_id_);
673
+ get().hideElements([edgeId]);
674
+ const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
675
+ targetNodeId,
676
+ "out",
677
+ edges,
678
+ true
679
+ );
680
+ devLog3("[GraphExploration] hideEdgeForwardPath - traversal complete", {
681
+ edgeId,
682
+ nodesToHide: nodesToHide.size,
683
+ edgesToHide: edgesToHide.size
684
+ });
685
+ const allElementsToHide = [...nodesToHide, ...edgesToHide];
686
+ if (allElementsToHide.length > 0) get().hideElements(allElementsToHide);
687
+ },
688
+ hideEdgeReversePath: (edgeId) => {
689
+ devLog3(
690
+ "[GraphExploration] hideEdgeReversePath - hiding edge + source + upstream",
691
+ { edgeId }
692
+ );
693
+ const { edges } = stores.graphData.getState();
694
+ const edge = edges.find((e) => String(e.id_) === edgeId);
695
+ if (!edge) {
696
+ devLog3("[GraphExploration] hideEdgeReversePath - edge not found", {
697
+ edgeId
698
+ });
699
+ return;
700
+ }
701
+ const sourceNodeId = String(edge.start_id_);
702
+ get().hideElements([edgeId]);
703
+ const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
704
+ sourceNodeId,
705
+ "in",
706
+ edges,
707
+ true
708
+ );
709
+ devLog3("[GraphExploration] hideEdgeReversePath - traversal complete", {
710
+ edgeId,
711
+ nodesToHide: nodesToHide.size,
712
+ edgesToHide: edgesToHide.size
713
+ });
714
+ const allElementsToHide = [...nodesToHide, ...edgesToHide];
715
+ if (allElementsToHide.length > 0) get().hideElements(allElementsToHide);
716
+ },
717
+ focusOnEdgeType: (edgeId) => {
718
+ devLog3(
719
+ "[GraphExploration] focusOnEdgeType - show only edges of this type and connected nodes",
720
+ { edgeId }
721
+ );
722
+ const graphData = stores.graphData.getState();
723
+ const edge = graphData.edges.find((e) => String(e.id_) === edgeId);
724
+ if (!edge) {
725
+ devLog3("[GraphExploration] focusOnEdgeType - edge not found", {
726
+ edgeId
727
+ });
728
+ return;
729
+ }
730
+ const edgeLabel = edge.label_;
731
+ devLog3("[GraphExploration] focusOnEdgeType - filtering by label", {
732
+ edgeId,
733
+ edgeLabel
734
+ });
735
+ const edgesOfType = graphData.edges.filter((e) => e.label_ === edgeLabel);
736
+ const connectedNodeIds = /* @__PURE__ */ new Set();
737
+ const visibleEdgeIds = /* @__PURE__ */ new Set();
738
+ for (const e of edgesOfType) {
739
+ connectedNodeIds.add(String(e.start_id_));
740
+ connectedNodeIds.add(String(e.end_id_));
741
+ visibleEdgeIds.add(String(e.id_));
742
+ }
743
+ const nodesToHide = [];
744
+ const edgesToHide = [];
745
+ for (const node of graphData.nodes) {
746
+ const nodeId = String(node.id_);
747
+ if (!connectedNodeIds.has(nodeId)) {
748
+ nodesToHide.push(nodeId);
749
+ }
750
+ }
751
+ for (const e of graphData.edges) {
752
+ const eId = String(e.id_);
753
+ if (!visibleEdgeIds.has(eId)) {
754
+ edgesToHide.push(eId);
755
+ }
756
+ }
757
+ devLog3("[GraphExploration] focusOnEdgeType - hiding elements", {
758
+ edgeLabel,
759
+ visibleEdges: visibleEdgeIds.size,
760
+ visibleNodes: connectedNodeIds.size,
761
+ hidingNodes: nodesToHide.length,
762
+ hidingEdges: edgesToHide.length
763
+ });
764
+ const allElementsToHide = [...nodesToHide, ...edgesToHide];
765
+ if (allElementsToHide.length > 0) {
766
+ get().hideElements(allElementsToHide);
767
+ }
768
+ },
769
+ focusEdge: (edgeId) => {
770
+ devLog3(
771
+ "[GraphExploration] focusEdge - unified focus with auto-direction detection",
772
+ { edgeId }
773
+ );
774
+ const graphData = stores.graphData.getState();
775
+ const selection = stores.selection.getState();
776
+ const edge = graphData.edges.find((e) => String(e.id_) === edgeId);
777
+ if (!edge) {
778
+ devLog3("[GraphExploration] focusEdge - edge not found", { edgeId });
779
+ return;
780
+ }
781
+ const endpoints = {
782
+ edgeLabel: edge.label_,
783
+ startNodeId: String(edge.start_id_),
784
+ endNodeId: String(edge.end_id_)
785
+ };
786
+ const { direction, referenceNodeId } = determineEdgeDirection(
787
+ endpoints,
788
+ selection.selectedNodeIds
789
+ );
790
+ devLog3("[GraphExploration] focusEdge - determined direction", {
791
+ edgeId,
792
+ edgeLabel: endpoints.edgeLabel,
793
+ direction,
794
+ referenceNodeId,
795
+ selectionMode: selection.selectedNodeIds.length === 1 ? "single-node" : "traditional"
796
+ });
797
+ const focusNodeIds = buildFocusSet(
798
+ referenceNodeId,
799
+ direction,
800
+ endpoints.edgeLabel,
801
+ graphData.edges
802
+ );
803
+ devLog3("[GraphExploration] focusEdge - found nodes", {
804
+ edgeLabel: endpoints.edgeLabel,
805
+ direction,
806
+ referenceNodeId,
807
+ totalNodes: focusNodeIds.size
808
+ });
809
+ set({
810
+ focusedNodeIds: focusNodeIds,
811
+ isFocusMode: true
812
+ });
813
+ },
814
+ resetView: () => {
815
+ devLog3("[GraphExploration] resetView - clearing all exploration state");
816
+ set({
817
+ focusedNodeIds: /* @__PURE__ */ new Set(),
818
+ isFocusMode: false,
819
+ hiddenElementIds: /* @__PURE__ */ new Set()
820
+ // Note: We keep expandedNodeIds - those nodes are still in the data
821
+ });
822
+ },
823
+ resetForNewQuery: () => {
824
+ devLog3(
825
+ "[GraphExploration] resetForNewQuery - clearing ALL exploration state for new query"
826
+ );
827
+ set({
828
+ focusedNodeIds: /* @__PURE__ */ new Set(),
829
+ isFocusMode: false,
830
+ hiddenElementIds: /* @__PURE__ */ new Set(),
831
+ expandedNodeIds: /* @__PURE__ */ new Set()
832
+ });
833
+ },
834
+ markNodeExpanded: (nodeId) => {
835
+ const current = get().expandedNodeIds;
836
+ const updated = new Set(current);
837
+ updated.add(nodeId);
838
+ devLog3("[GraphExploration] markNodeExpanded", { nodeId });
839
+ set({ expandedNodeIds: updated });
840
+ },
841
+ isNodeExpanded: (nodeId) => {
842
+ return get().expandedNodeIds.has(nodeId);
843
+ },
844
+ expandNode: async (nodeId) => {
845
+ devLog3("[GraphExploration] expandNode", { nodeId });
846
+ try {
847
+ const result = await stores.expandNode(Number(nodeId));
848
+ devLog3("[GraphExploration] Expansion API result", {
849
+ nodeId,
850
+ newNodes: result.nodes.length,
851
+ newEdges: result.edges.length
852
+ });
853
+ if (result.nodes.length === 0 && result.edges.length === 0) {
854
+ devLog3("[GraphExploration] No connected nodes found", { nodeId });
855
+ return;
856
+ }
857
+ const existingNodes = stores.graphData.getState().nodes;
858
+ const colorPalette = buildColorPaletteFromNodes([
859
+ ...existingNodes,
860
+ ...result.nodes
861
+ ]);
862
+ const enrichedNodes = enrichNodes(
863
+ result.nodes,
864
+ colorPalette,
865
+ stores.getLabelDisplayMap?.()
866
+ );
867
+ const enrichedEdges = enrichEdges(result.edges);
868
+ stores.graphData.getState().mergeExpandedData(enrichedNodes, enrichedEdges);
869
+ const graphFilterState = stores.graphFilter.getState();
870
+ const currentFilters = graphFilterState.nodeLabelFilters;
871
+ if (currentFilters.length > 0 && enrichedNodes.length > 0) {
872
+ const newLabels = new Set(enrichedNodes.map((n) => n.label_));
873
+ const labelsToAdd = Array.from(newLabels).filter(
874
+ (label) => !currentFilters.includes(label)
875
+ );
876
+ if (labelsToAdd.length > 0) {
877
+ devLog3(
878
+ "[GraphExploration] Adding expanded node labels to active filters",
879
+ {
880
+ nodeId,
881
+ labelsToAdd,
882
+ currentFilters
883
+ }
884
+ );
885
+ stores.graphFilter.getState().setNodeLabelFilters([...currentFilters, ...labelsToAdd]);
886
+ }
887
+ }
888
+ get().markNodeExpanded(nodeId);
889
+ const currentState = get();
890
+ if (currentState.isFocusMode) {
891
+ const newFocusedIds = new Set(currentState.focusedNodeIds);
892
+ for (const node of enrichedNodes) {
893
+ newFocusedIds.add(String(node.id_));
894
+ }
895
+ devLog3("[GraphExploration] Adding expanded nodes to focus set", {
896
+ nodeId,
897
+ previousFocusCount: currentState.focusedNodeIds.size,
898
+ addedToFocus: enrichedNodes.length,
899
+ newFocusCount: newFocusedIds.size
900
+ });
901
+ set({ focusedNodeIds: newFocusedIds });
902
+ }
903
+ devLog3("[GraphExploration] Expansion complete", {
904
+ nodeId,
905
+ addedNodes: enrichedNodes.length,
906
+ addedEdges: enrichedEdges.length
907
+ });
908
+ } catch (error) {
909
+ devLog3("[GraphExploration] Expansion failed", { nodeId, error });
910
+ throw error;
911
+ }
912
+ },
913
+ addNodeById: async (nodeId, autoFocus = false) => {
914
+ const nodeIdStr = String(nodeId);
915
+ devLog3("[GraphExploration] addNodeById", { nodeId, autoFocus });
916
+ try {
917
+ const existingNodes = stores.graphData.getState().nodes;
918
+ if (handleExistingNode(
919
+ nodeId,
920
+ nodeIdStr,
921
+ existingNodes,
922
+ autoFocus,
923
+ stores.graphFocus
924
+ )) {
925
+ return;
926
+ }
927
+ const enrichedNodes = await fetchAndEnrichNode(
928
+ nodeId,
929
+ existingNodes,
930
+ stores
931
+ );
932
+ ensureNodeLabelInFilters(enrichedNodes, nodeId, stores.graphFilter);
933
+ if (autoFocus) {
934
+ devLog3("[GraphExploration] Requesting focus for newly added node", {
935
+ nodeId
936
+ });
937
+ stores.graphFocus.getState().requestFocus(nodeIdStr, "node", false);
938
+ } else {
939
+ devLog3("[GraphExploration] Skipping focus for newly added node", {
940
+ nodeId
941
+ });
942
+ }
943
+ const currentState = get();
944
+ if (currentState.isFocusMode) {
945
+ const newFocusedIds = new Set(currentState.focusedNodeIds);
946
+ newFocusedIds.add(nodeIdStr);
947
+ devLog3("[GraphExploration] Adding node to focus set", {
948
+ nodeId,
949
+ previousFocusCount: currentState.focusedNodeIds.size,
950
+ newFocusCount: newFocusedIds.size
951
+ });
952
+ set({ focusedNodeIds: newFocusedIds });
953
+ }
954
+ devLog3("[GraphExploration] addNodeById complete", {
955
+ nodeId,
956
+ addedNodes: enrichedNodes.length,
957
+ autoFocus
958
+ });
959
+ } catch (error) {
960
+ devLog3("[GraphExploration] addNodeById failed", { nodeId, error });
961
+ throw error;
962
+ }
963
+ }
964
+ }));
965
+ }
966
+
967
+ // src/stores/GraphFocus.ts
968
+ import { create as create5 } from "zustand";
969
+ import { devLog as devLog4 } from "@petrarca/sonnet-core";
970
+ function buildStore() {
971
+ return (set, get) => ({
972
+ focusAction: null,
973
+ overrideElementIds: /* @__PURE__ */ new Set(),
974
+ requestFocus: (elementId, elementType, shouldSelect) => {
975
+ const action = {
976
+ elementId,
977
+ elementType,
978
+ action: shouldSelect ? "focus-and-select" : "focus",
979
+ timestamp: Date.now()
980
+ };
981
+ devLog4("[GraphFocusStore] requestFocus", {
982
+ elementId,
983
+ elementType,
984
+ action: action.action
985
+ });
986
+ set({ focusAction: action });
987
+ },
988
+ clear: () => {
989
+ const prev = get().focusAction;
990
+ if (prev) {
991
+ devLog4("[GraphFocusStore] clear", { prevElementId: prev.elementId });
992
+ }
993
+ set({ focusAction: null, overrideElementIds: /* @__PURE__ */ new Set() });
994
+ },
995
+ clearOverrides: () => {
996
+ devLog4("[GraphFocusStore] clearOverrides");
997
+ set({ overrideElementIds: /* @__PURE__ */ new Set() });
998
+ }
999
+ });
1000
+ }
1001
+ function createGraphFocusStore() {
1002
+ return create5()(buildStore());
1003
+ }
1004
+ var useGraphFocus = createGraphFocusStore();
1005
+ var GraphFocus_default = useGraphFocus;
1006
+
1007
+ // src/stores/ActionPanelStore.ts
1008
+ import { create as create6 } from "zustand";
1009
+ import { devLog as devLog5 } from "@petrarca/sonnet-core";
1010
+ function buildStore2() {
1011
+ return (set, get) => ({
1012
+ state: "visible",
1013
+ setState: (state) => {
1014
+ devLog5("[ActionPanelStore] setState", { state });
1015
+ set({ state });
1016
+ },
1017
+ hide: () => {
1018
+ devLog5("[ActionPanelStore] hide");
1019
+ set({ state: "hidden" });
1020
+ },
1021
+ show: () => {
1022
+ devLog5("[ActionPanelStore] show");
1023
+ set({ state: "visible" });
1024
+ },
1025
+ isHidden: () => get().state === "hidden"
1026
+ });
1027
+ }
1028
+ function createActionPanelStore() {
1029
+ return create6()(buildStore2());
1030
+ }
1031
+ var useActionPanelStore = createActionPanelStore();
1032
+
1033
+ // src/hooks/useWorkspaceStores.ts
1034
+ import { createContext, useContext } from "react";
1035
+ var GraphStoresContext = createContext(null);
1036
+ function useGraphStores() {
1037
+ const ctx = useContext(GraphStoresContext);
1038
+ if (!ctx) {
1039
+ throw new Error(
1040
+ "useGraphStores must be used within a GraphStoresProvider. Wrap your graph UI with <GraphStoresContext.Provider value={...}>."
1041
+ );
1042
+ }
1043
+ return ctx;
1044
+ }
1045
+ function useWorkspaceGraphFilter() {
1046
+ return useGraphStores().graphFilter;
1047
+ }
1048
+ function useWorkspaceGraphData() {
1049
+ return useGraphStores().graphData;
1050
+ }
1051
+ function useWorkspaceGraphExploration() {
1052
+ return useGraphStores().graphExploration;
1053
+ }
1054
+ function useWorkspaceSelection() {
1055
+ return useGraphStores().selection;
1056
+ }
1057
+
1058
+ // src/hooks/useLiveGraphData.ts
1059
+ import { useMemo, useEffect } from "react";
1060
+ import { devLog as devLog6 } from "@petrarca/sonnet-core";
1061
+ function useLiveGraphData() {
1062
+ const useGraphFilter2 = useWorkspaceGraphFilter();
1063
+ const useGraphData2 = useWorkspaceGraphData();
1064
+ const useGraphExploration = useWorkspaceGraphExploration();
1065
+ const lastDataChangeTime = useGraphFilter2((s) => s.lastDataChangeTime);
1066
+ const filterLabels = useGraphFilter2((s) => s.nodeLabelFilters);
1067
+ const allLabelsDisabled = useGraphFilter2((s) => s.allLabelsDisabled);
1068
+ const edgeFilterLabels = useGraphFilter2((s) => s.edgeLabelFilters);
1069
+ const allEdgeLabelsDisabled = useGraphFilter2((s) => s.allEdgeLabelsDisabled);
1070
+ const overrideElementIds = GraphFocus_default((s) => s.overrideElementIds);
1071
+ const isFocusMode = useGraphExploration((s) => s.isFocusMode);
1072
+ const focusedNodeIds = useGraphExploration((s) => s.focusedNodeIds);
1073
+ const hiddenElementIds = useGraphExploration((s) => s.hiddenElementIds);
1074
+ const baseNodes = useGraphData2((s) => s.nodes);
1075
+ const baseEdges = useGraphData2((s) => s.edges);
1076
+ const stats = useGraphData2((s) => s.stats);
1077
+ const values = useGraphData2((s) => s.values);
1078
+ const isValueSet = useGraphData2((s) => s.isValueSet);
1079
+ devLog6("[useLiveGraphData] Store data", {
1080
+ baseNodes: baseNodes.length,
1081
+ baseEdges: baseEdges.length,
1082
+ isFocusMode,
1083
+ focusedCount: focusedNodeIds.size,
1084
+ hiddenCount: hiddenElementIds.size
1085
+ });
1086
+ useEffect(() => {
1087
+ GraphFocus_default.getState().clearOverrides();
1088
+ }, [lastDataChangeTime]);
1089
+ const allLabelCounts = useMemo(() => {
1090
+ if (isValueSet) return {};
1091
+ const counts = {};
1092
+ for (const n of baseNodes) {
1093
+ const l = n?.label_;
1094
+ if (l) counts[l] = (counts[l] || 0) + 1;
1095
+ }
1096
+ return counts;
1097
+ }, [baseNodes, isValueSet]);
1098
+ const colorPalette = useMemo(() => {
1099
+ if (isValueSet) return {};
1100
+ return buildColorPalette(allLabelCounts);
1101
+ }, [allLabelCounts, isValueSet]);
1102
+ const availableNodes = useMemo(() => {
1103
+ let available = baseNodes;
1104
+ if (isFocusMode && focusedNodeIds.size > 0) {
1105
+ available = available.filter(
1106
+ (n) => focusedNodeIds.has(String(n.id_))
1107
+ );
1108
+ }
1109
+ if (hiddenElementIds.size > 0) {
1110
+ available = available.filter(
1111
+ (n) => !hiddenElementIds.has(String(n.id_))
1112
+ );
1113
+ }
1114
+ return available;
1115
+ }, [baseNodes, isFocusMode, focusedNodeIds, hiddenElementIds]);
1116
+ const availableEdges = useMemo(() => {
1117
+ const availableNodeIds = new Set(
1118
+ availableNodes.map((n) => String(n.id_))
1119
+ );
1120
+ let available = baseEdges.filter(
1121
+ (e) => availableNodeIds.has(String(e.start_id_)) && availableNodeIds.has(String(e.end_id_))
1122
+ );
1123
+ if (hiddenElementIds.size > 0) {
1124
+ available = available.filter(
1125
+ (e) => !hiddenElementIds.has(String(e.id_))
1126
+ );
1127
+ }
1128
+ return available;
1129
+ }, [baseEdges, availableNodes, hiddenElementIds]);
1130
+ const labelCounts = useMemo(() => {
1131
+ if (isValueSet) return {};
1132
+ const counts = {};
1133
+ for (const n of availableNodes) {
1134
+ const l = n?.label_;
1135
+ if (l) counts[l] = (counts[l] || 0) + 1;
1136
+ }
1137
+ return counts;
1138
+ }, [availableNodes, isValueSet]);
1139
+ const edgeLabelCounts = useMemo(() => {
1140
+ if (isValueSet) return {};
1141
+ const counts = {};
1142
+ for (const e of availableEdges) {
1143
+ const l = e?.label_;
1144
+ if (l) counts[l] = (counts[l] || 0) + 1;
1145
+ }
1146
+ return counts;
1147
+ }, [availableEdges, isValueSet]);
1148
+ const filteredNodes = useMemo(() => {
1149
+ let filtered;
1150
+ if (allLabelsDisabled) {
1151
+ filtered = [];
1152
+ } else if (filterLabels && filterLabels.length > 0) {
1153
+ const setLabels = new Set(filterLabels);
1154
+ filtered = baseNodes.filter(
1155
+ (n) => setLabels.has(n.label_)
1156
+ );
1157
+ } else {
1158
+ filtered = baseNodes;
1159
+ }
1160
+ if (isFocusMode && focusedNodeIds.size > 0) {
1161
+ devLog6("[useLiveGraphData] Applying focus mode filter", {
1162
+ focusedCount: focusedNodeIds.size,
1163
+ beforeFilter: filtered.length
1164
+ });
1165
+ filtered = filtered.filter((n) => focusedNodeIds.has(String(n.id_)));
1166
+ devLog6("[useLiveGraphData] After focus mode filter", {
1167
+ afterFilter: filtered.length
1168
+ });
1169
+ }
1170
+ if (hiddenElementIds.size > 0) {
1171
+ devLog6("[useLiveGraphData] Applying hidden elements filter", {
1172
+ hiddenCount: hiddenElementIds.size,
1173
+ beforeFilter: filtered.length
1174
+ });
1175
+ filtered = filtered.filter((n) => !hiddenElementIds.has(String(n.id_)));
1176
+ devLog6("[useLiveGraphData] After hidden elements filter", {
1177
+ afterFilter: filtered.length
1178
+ });
1179
+ }
1180
+ if (overrideElementIds.size > 0) {
1181
+ const filteredIds = new Set(
1182
+ filtered.map((n) => String(n.id_))
1183
+ );
1184
+ const overrideNodes = baseNodes.filter(
1185
+ (n) => overrideElementIds.has(String(n.id_)) && !filteredIds.has(String(n.id_))
1186
+ );
1187
+ if (overrideNodes.length > 0) {
1188
+ devLog6("[useLiveGraphData] Adding override nodes", {
1189
+ overrideCount: overrideNodes.length,
1190
+ overrideIds: overrideNodes.map((n) => n.id_),
1191
+ totalAfterOverride: filtered.length + overrideNodes.length
1192
+ });
1193
+ filtered = [...filtered, ...overrideNodes];
1194
+ }
1195
+ }
1196
+ return filtered;
1197
+ }, [
1198
+ baseNodes,
1199
+ filterLabels,
1200
+ allLabelsDisabled,
1201
+ isFocusMode,
1202
+ focusedNodeIds,
1203
+ hiddenElementIds,
1204
+ overrideElementIds
1205
+ ]);
1206
+ const filteredEdges = useMemo(() => {
1207
+ let edgesStage;
1208
+ if (allLabelsDisabled || allEdgeLabelsDisabled) {
1209
+ edgesStage = [];
1210
+ } else {
1211
+ const nodeIds = new Set(
1212
+ filteredNodes.map((n) => String(n.id_))
1213
+ );
1214
+ edgesStage = baseEdges.filter(
1215
+ (e) => nodeIds.has(String(e.start_id_)) && nodeIds.has(String(e.end_id_))
1216
+ );
1217
+ if (edgeFilterLabels && edgeFilterLabels.length > 0) {
1218
+ const setEdge = new Set(edgeFilterLabels);
1219
+ edgesStage = edgesStage.filter((e) => setEdge.has(e.label_));
1220
+ }
1221
+ }
1222
+ if (hiddenElementIds.size > 0) {
1223
+ devLog6("[useLiveGraphData] Applying hidden elements filter to edges", {
1224
+ hiddenCount: hiddenElementIds.size,
1225
+ beforeFilter: edgesStage.length
1226
+ });
1227
+ edgesStage = edgesStage.filter(
1228
+ (e) => !hiddenElementIds.has(String(e.id_))
1229
+ );
1230
+ devLog6("[useLiveGraphData] After hidden elements filter (edges)", {
1231
+ afterFilter: edgesStage.length
1232
+ });
1233
+ }
1234
+ if (overrideElementIds.size > 0) {
1235
+ const filteredIds = new Set(
1236
+ edgesStage.map((e) => String(e.id_))
1237
+ );
1238
+ const overrideEdges = baseEdges.filter(
1239
+ (e) => overrideElementIds.has(String(e.id_)) && !filteredIds.has(String(e.id_))
1240
+ );
1241
+ if (overrideEdges.length > 0) {
1242
+ devLog6("[useLiveGraphData] Adding override edges", {
1243
+ overrideCount: overrideEdges.length,
1244
+ overrideIds: overrideEdges.map((e) => e.id_),
1245
+ totalAfterOverride: edgesStage.length + overrideEdges.length
1246
+ });
1247
+ edgesStage = [...edgesStage, ...overrideEdges];
1248
+ }
1249
+ }
1250
+ return edgesStage;
1251
+ }, [
1252
+ baseEdges,
1253
+ filteredNodes,
1254
+ allLabelsDisabled,
1255
+ allEdgeLabelsDisabled,
1256
+ edgeFilterLabels,
1257
+ hiddenElementIds,
1258
+ overrideElementIds
1259
+ ]);
1260
+ const baseNormalized = useMemo(() => {
1261
+ if (isValueSet) return EMPTY_NORMALIZED;
1262
+ return normalizeGraph(baseNodes, baseEdges);
1263
+ }, [isValueSet, baseNodes, baseEdges]);
1264
+ const normalized = useMemo(() => {
1265
+ if (isValueSet) return EMPTY_NORMALIZED;
1266
+ return normalizeGraph(filteredNodes, filteredEdges);
1267
+ }, [isValueSet, filteredNodes, filteredEdges]);
1268
+ const nodes = useMemo(
1269
+ () => normalized.nodeIds.map((id) => normalized.nodesById[id]),
1270
+ [normalized]
1271
+ );
1272
+ const edges = useMemo(
1273
+ () => normalized.edgeIds.map((id) => normalized.edgesById[id]),
1274
+ [normalized]
1275
+ );
1276
+ const adjustedStats = useMemo(() => {
1277
+ if (!stats) return void 0;
1278
+ return {
1279
+ ...stats,
1280
+ nodes: availableNodes.length,
1281
+ edges: availableEdges.length
1282
+ };
1283
+ }, [stats, availableNodes.length, availableEdges.length]);
1284
+ useEffect(() => {
1285
+ const { clearOverrides } = GraphFocus_default.getState();
1286
+ clearOverrides();
1287
+ }, [
1288
+ filterLabels,
1289
+ allLabelsDisabled,
1290
+ edgeFilterLabels,
1291
+ allEdgeLabelsDisabled
1292
+ ]);
1293
+ return {
1294
+ nodes,
1295
+ edges,
1296
+ stats: adjustedStats,
1297
+ labelCounts,
1298
+ edgeLabelCounts,
1299
+ values,
1300
+ colorPalette,
1301
+ normalized,
1302
+ baseNormalized
1303
+ };
1304
+ }
1305
+
1306
+ // src/hooks/useReagraphData.ts
1307
+ import { useMemo as useMemo2 } from "react";
1308
+ function toReagraphNode(visualNode) {
1309
+ return {
1310
+ id: String(visualNode.id_),
1311
+ label: visualNode.visual.displayName,
1312
+ fill: visualNode.visual.color,
1313
+ size: visualNode.visual.size,
1314
+ icon: visualNode.visual.icon,
1315
+ // Embed full visual node so event handlers can access all domain data
1316
+ data: visualNode
1317
+ };
1318
+ }
1319
+ function toReagraphEdge(visualEdge) {
1320
+ return {
1321
+ id: String(visualEdge.id_),
1322
+ source: String(visualEdge.start_id_),
1323
+ target: String(visualEdge.end_id_),
1324
+ label: visualEdge.label_,
1325
+ fill: visualEdge.visual.color,
1326
+ size: visualEdge.visual.size ?? 3,
1327
+ dashed: visualEdge.visual.dashed,
1328
+ // Embed full visual edge so event handlers can access all domain data
1329
+ data: visualEdge
1330
+ };
1331
+ }
1332
+ function useReagraphData() {
1333
+ const live = useLiveGraphData();
1334
+ const nodes = useMemo2(
1335
+ () => live.nodes.map(toReagraphNode),
1336
+ [live.nodes]
1337
+ );
1338
+ const edges = useMemo2(
1339
+ () => live.edges.map(toReagraphEdge),
1340
+ [live.edges]
1341
+ );
1342
+ return {
1343
+ nodes,
1344
+ edges,
1345
+ stats: live.stats,
1346
+ colorPalette: live.colorPalette
1347
+ };
1348
+ }
1349
+
1350
+ // src/hooks/useCytoscapeData.ts
1351
+ import { useMemo as useMemo3 } from "react";
1352
+ function toCytoscapeNode(visualNode) {
1353
+ const color = visualNode.visual.color ?? "#4285F4";
1354
+ const size = visualNode.visual.size ?? 40;
1355
+ return {
1356
+ data: {
1357
+ id: String(visualNode.id_),
1358
+ label: visualNode.visual.displayName,
1359
+ id_: visualNode.id_,
1360
+ label_: visualNode.label_,
1361
+ properties_: visualNode.properties_,
1362
+ displayName: visualNode.visual.displayName,
1363
+ color,
1364
+ size,
1365
+ icon: visualNode.visual.icon,
1366
+ // Full node reference for context menu and event handlers
1367
+ visualGraph: visualNode
1368
+ }
1369
+ // No inline styles — stylesheet handles all styling via data mappers
1370
+ };
1371
+ }
1372
+ function toCytoscapeEdge(visualEdge) {
1373
+ return {
1374
+ data: {
1375
+ id: String(visualEdge.id_),
1376
+ source: String(visualEdge.start_id_),
1377
+ target: String(visualEdge.end_id_),
1378
+ label: visualEdge.label_,
1379
+ id_: visualEdge.id_,
1380
+ label_: visualEdge.label_,
1381
+ start_id_: visualEdge.start_id_,
1382
+ end_id_: visualEdge.end_id_,
1383
+ properties_: visualEdge.properties_,
1384
+ color: visualEdge.visual.color ?? "#9E9E9E",
1385
+ size: visualEdge.visual.size ?? 3,
1386
+ // Only set dashed when true — falsy values omitted to avoid stylesheet noise
1387
+ dashed: visualEdge.visual.dashed || void 0
1388
+ }
1389
+ // No inline styles — stylesheet handles all styling via data mappers
1390
+ };
1391
+ }
1392
+ function useCytoscapeData() {
1393
+ const live = useLiveGraphData();
1394
+ const elements = useMemo3(
1395
+ () => ({
1396
+ nodes: live.nodes.map(toCytoscapeNode),
1397
+ edges: live.edges.map(toCytoscapeEdge)
1398
+ }),
1399
+ [live.nodes, live.edges]
1400
+ );
1401
+ return {
1402
+ elements,
1403
+ stats: live.stats,
1404
+ colorPalette: live.colorPalette
1405
+ };
1406
+ }
1407
+
1408
+ // src/hooks/useGraphRendererContext.ts
1409
+ import { createContext as createContext2, useContext as useContext2 } from "react";
1410
+ var GraphRendererContext = createContext2(null);
1411
+ function useGraphRenderer() {
1412
+ const ctx = useContext2(GraphRendererContext);
1413
+ if (!ctx) {
1414
+ throw new Error(
1415
+ "useGraphRenderer must be used within a GraphRendererProvider"
1416
+ );
1417
+ }
1418
+ return ctx;
1419
+ }
1420
+
1421
+ // src/hooks/graphRendererProvider.tsx
1422
+ import { useState, useCallback } from "react";
1423
+ import { jsx } from "react/jsx-runtime";
1424
+ function GraphRendererProvider({
1425
+ children,
1426
+ defaultRenderer = "cytoscape",
1427
+ onRendererChange
1428
+ }) {
1429
+ const [renderer, setRendererState] = useState(defaultRenderer);
1430
+ const setRenderer = useCallback(
1431
+ (newRenderer) => {
1432
+ setRendererState(newRenderer);
1433
+ onRendererChange?.(newRenderer);
1434
+ },
1435
+ [onRendererChange]
1436
+ );
1437
+ return /* @__PURE__ */ jsx(GraphRendererContext.Provider, { value: { renderer, setRenderer }, children });
1438
+ }
1439
+
1440
+ // src/components/rea/ReaGraph.tsx
1441
+ import {
1442
+ useEffect as useEffect2,
1443
+ useRef,
1444
+ useState as useState3,
1445
+ useCallback as useCallback2
1446
+ } from "react";
1447
+ import { devLog as devLog8 } from "@petrarca/sonnet-core";
1448
+ import {
1449
+ GraphCanvas,
1450
+ useSelection as useSelection2
1451
+ } from "reagraph";
1452
+
1453
+ // src/components/rea/theme.ts
1454
+ import { lightTheme } from "reagraph";
1455
+ var highlightColor = "#ffb512";
1456
+ var highlightColorText = "#c98a02";
1457
+ var theme = {
1458
+ ...lightTheme,
1459
+ node: {
1460
+ ...lightTheme.node,
1461
+ activeFill: highlightColor,
1462
+ label: {
1463
+ ...lightTheme.node.label,
1464
+ activeColor: highlightColorText
1465
+ }
1466
+ },
1467
+ ring: {
1468
+ ...lightTheme.ring,
1469
+ activeFill: highlightColor
1470
+ },
1471
+ edge: {
1472
+ ...lightTheme.edge,
1473
+ activeFill: highlightColor,
1474
+ label: {
1475
+ ...lightTheme.edge.label,
1476
+ activeColor: highlightColorText
1477
+ }
1478
+ },
1479
+ arrow: {
1480
+ ...lightTheme.arrow,
1481
+ activeFill: highlightColor
1482
+ }
1483
+ };
1484
+
1485
+ // src/components/rea/ReaGraph.tsx
1486
+ import { cn } from "@petrarca/sonnet-core";
1487
+
1488
+ // src/components/rea/LayoutAndCameraControls.tsx
1489
+ import {
1490
+ Button as Button2,
1491
+ DropdownMenu as DropdownMenu2,
1492
+ DropdownMenuContent as DropdownMenuContent2,
1493
+ DropdownMenuItem as DropdownMenuItem2,
1494
+ DropdownMenuLabel,
1495
+ DropdownMenuSeparator,
1496
+ DropdownMenuTrigger as DropdownMenuTrigger2,
1497
+ SimpleTooltip as SimpleTooltip2
1498
+ } from "@petrarca/sonnet-ui";
1499
+ import { LayoutPanelTop, Maximize, Shrink, ArrowUp, Eye } from "lucide-react";
1500
+ import { useState as useState2 } from "react";
1501
+
1502
+ // src/components/shared/RendererDropdown.tsx
1503
+ import {
1504
+ Button,
1505
+ DropdownMenu,
1506
+ DropdownMenuContent,
1507
+ DropdownMenuItem,
1508
+ DropdownMenuTrigger,
1509
+ SimpleTooltip
1510
+ } from "@petrarca/sonnet-ui";
1511
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1512
+ function RendererDropdown() {
1513
+ const { renderer, setRenderer } = useGraphRenderer();
1514
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1515
+ /* @__PURE__ */ jsx2(SimpleTooltip, { label: "Renderer", side: "left", children: /* @__PURE__ */ jsx2(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx2(Button, { variant: "ghost", size: "icon", children: renderer === "reagraph" ? "RG" : "CY" }) }) }),
1516
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { side: "left", align: "start", children: [
1517
+ /* @__PURE__ */ jsx2(
1518
+ DropdownMenuItem,
1519
+ {
1520
+ onClick: () => setRenderer("reagraph"),
1521
+ className: renderer === "reagraph" ? "text-blue-600" : "",
1522
+ children: "Reagraph"
1523
+ }
1524
+ ),
1525
+ /* @__PURE__ */ jsx2(
1526
+ DropdownMenuItem,
1527
+ {
1528
+ onClick: () => setRenderer("cytoscape"),
1529
+ className: renderer === "cytoscape" ? "text-blue-600" : "",
1530
+ children: "Cytoscape"
1531
+ }
1532
+ )
1533
+ ] })
1534
+ ] });
1535
+ }
1536
+
1537
+ // src/components/rea/LayoutAndCameraControls.tsx
1538
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1539
+ function PathSelectionDropdown({
1540
+ setPathSelectionType
1541
+ }) {
1542
+ return /* @__PURE__ */ jsxs2(DropdownMenu2, { children: [
1543
+ /* @__PURE__ */ jsx3(SimpleTooltip2, { label: "Path Selection Type", side: "left", children: /* @__PURE__ */ jsx3(DropdownMenuTrigger2, { asChild: true, children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx3(ArrowUp, {}) }) }) }),
1544
+ /* @__PURE__ */ jsxs2(DropdownMenuContent2, { side: "left", align: "start", children: [
1545
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => setPathSelectionType("direct"), children: "None" }),
1546
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => setPathSelectionType("all"), children: "All Paths" }),
1547
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => setPathSelectionType("in"), children: "Inbound" }),
1548
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => setPathSelectionType("out"), children: "Outbound" })
1549
+ ] })
1550
+ ] });
1551
+ }
1552
+ function LayoutDropdown({ updateLayout }) {
1553
+ const [showTooltip, setShowTooltip] = useState2(true);
1554
+ const handleSelect = (layout) => {
1555
+ updateLayout(layout);
1556
+ setShowTooltip(false);
1557
+ setTimeout(() => setShowTooltip(true), 500);
1558
+ };
1559
+ const trigger = /* @__PURE__ */ jsx3(DropdownMenuTrigger2, { asChild: true, children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx3(LayoutPanelTop, {}) }) });
1560
+ return /* @__PURE__ */ jsxs2(DropdownMenu2, { children: [
1561
+ showTooltip ? /* @__PURE__ */ jsx3(SimpleTooltip2, { label: "Layout", side: "left", children: trigger }) : trigger,
1562
+ /* @__PURE__ */ jsxs2(DropdownMenuContent2, { side: "left", align: "start", children: [
1563
+ /* @__PURE__ */ jsx3(DropdownMenuLabel, { children: "Force-directed Layouts" }),
1564
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("forceDirected2d"), children: "Force Directed" }),
1565
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("forceatlas2"), children: "Force Atlas 2" }),
1566
+ /* @__PURE__ */ jsx3(DropdownMenuSeparator, {}),
1567
+ /* @__PURE__ */ jsx3(DropdownMenuLabel, { children: "Tree Layouts" }),
1568
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("treeTd2d"), children: "Tree Top-Down" }),
1569
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("treeLr2d"), children: "Tree Left-Right" }),
1570
+ /* @__PURE__ */ jsx3(DropdownMenuSeparator, {}),
1571
+ /* @__PURE__ */ jsx3(DropdownMenuLabel, { children: "Circular & Radial Layouts" }),
1572
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("circular2d"), children: "Circular" }),
1573
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("radialOut2d"), children: "Radial Out" }),
1574
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("concentric2d"), children: "Concentric" }),
1575
+ /* @__PURE__ */ jsx3(DropdownMenuSeparator, {}),
1576
+ /* @__PURE__ */ jsx3(DropdownMenuLabel, { children: "Utility Layouts" }),
1577
+ /* @__PURE__ */ jsx3(DropdownMenuItem2, { onClick: () => handleSelect("nooverlap"), children: "No Overlap" })
1578
+ ] })
1579
+ ] });
1580
+ }
1581
+ function ViewControls({
1582
+ fullscreen,
1583
+ toggleFullscreen,
1584
+ fitToScreen,
1585
+ hasHiddenElements,
1586
+ isFocusMode,
1587
+ onResetView
1588
+ }) {
1589
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1590
+ /* @__PURE__ */ jsx3(SimpleTooltip2, { label: "Fullscreen", side: "left", children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", onClick: toggleFullscreen, children: !fullscreen ? /* @__PURE__ */ jsx3(Maximize, {}) : /* @__PURE__ */ jsx3(Shrink, {}) }) }),
1591
+ /* @__PURE__ */ jsx3(SimpleTooltip2, { label: "Fit to screen", side: "left", children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", onClick: fitToScreen, children: /* @__PURE__ */ jsx3(Shrink, {}) }) }),
1592
+ (hasHiddenElements || isFocusMode) && onResetView && /* @__PURE__ */ jsx3(
1593
+ SimpleTooltip2,
1594
+ {
1595
+ label: isFocusMode ? "Reset Focus" : "Show All Hidden",
1596
+ side: "left",
1597
+ children: /* @__PURE__ */ jsx3(Button2, { variant: "ghost", size: "icon", onClick: onResetView, children: /* @__PURE__ */ jsx3(Eye, {}) })
1598
+ }
1599
+ )
1600
+ ] });
1601
+ }
1602
+ function LayoutAndCameraControls({
1603
+ fitToScreen,
1604
+ fullscreen,
1605
+ toggleFullscreen,
1606
+ updateLayout,
1607
+ setPathSelectionType,
1608
+ hasHiddenElements = false,
1609
+ isFocusMode = false,
1610
+ onResetView,
1611
+ extraToolbarContent
1612
+ }) {
1613
+ return /* @__PURE__ */ jsxs2("div", { className: "absolute right-2 top-2 flex flex-col border", children: [
1614
+ /* @__PURE__ */ jsx3(RendererDropdown, {}),
1615
+ /* @__PURE__ */ jsx3(
1616
+ ViewControls,
1617
+ {
1618
+ fullscreen,
1619
+ toggleFullscreen,
1620
+ fitToScreen,
1621
+ hasHiddenElements,
1622
+ isFocusMode,
1623
+ onResetView
1624
+ }
1625
+ ),
1626
+ /* @__PURE__ */ jsx3(PathSelectionDropdown, { setPathSelectionType }),
1627
+ extraToolbarContent,
1628
+ /* @__PURE__ */ jsx3(LayoutDropdown, { updateLayout })
1629
+ ] });
1630
+ }
1631
+ var LayoutAndCameraControls_default = LayoutAndCameraControls;
1632
+
1633
+ // src/components/rea/ReaGraph.tsx
1634
+ import { Card as Card2 } from "@petrarca/sonnet-ui";
1635
+
1636
+ // src/components/rea/NodeHoverInfo.tsx
1637
+ import { Card, Separator } from "@petrarca/sonnet-ui";
1638
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1639
+ function NodeHoverInfo({ node }) {
1640
+ return /* @__PURE__ */ jsx4(Card, { className: "absolute top-2 left-1/2 -translate-x-1/2 bg-white shadow-md p-2", children: /* @__PURE__ */ jsxs3("div", { className: "flex flex-row items-center gap-2", children: [
1641
+ /* @__PURE__ */ jsx4(
1642
+ Separator,
1643
+ {
1644
+ orientation: "vertical",
1645
+ decorative: true,
1646
+ className: "h-10 w-1",
1647
+ style: { backgroundColor: node.fill }
1648
+ }
1649
+ ),
1650
+ /* @__PURE__ */ jsxs3("div", { children: [
1651
+ /* @__PURE__ */ jsxs3("p", { children: [
1652
+ /* @__PURE__ */ jsxs3("strong", { children: [
1653
+ node.data.label_,
1654
+ ": "
1655
+ ] }),
1656
+ node.label
1657
+ ] }),
1658
+ /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: node.data.id_ })
1659
+ ] })
1660
+ ] }) });
1661
+ }
1662
+ var NodeHoverInfo_default = NodeHoverInfo;
1663
+
1664
+ // src/components/rea/ReaContextMenu.tsx
1665
+ import React2 from "react";
1666
+ import { devLog as devLog7 } from "@petrarca/sonnet-core";
1667
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1668
+ function ReaContextMenu({
1669
+ data,
1670
+ onClose,
1671
+ elementType,
1672
+ useGraphExploration,
1673
+ onCopyNode
1674
+ }) {
1675
+ const handleFocus = () => {
1676
+ if (elementType === "node") {
1677
+ const nodeId = data.id;
1678
+ devLog7("[ReaContextMenu] Focus on node and neighborhood:", nodeId);
1679
+ useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId);
1680
+ } else {
1681
+ const startNodeId = data.data.start_id_;
1682
+ devLog7("[ReaContextMenu] Focus on edge start node:", startNodeId);
1683
+ useGraphExploration.getState().setFocusModeWithNeighborhood(startNodeId);
1684
+ }
1685
+ onClose();
1686
+ };
1687
+ const handleCopy = () => {
1688
+ devLog7("[ReaContextMenu] Copy to clipboard");
1689
+ if (elementType === "node") {
1690
+ const visualGraph = data.data?.visualGraph;
1691
+ if (visualGraph && onCopyNode) {
1692
+ onCopyNode(visualGraph);
1693
+ } else if (!visualGraph) {
1694
+ const nodeText = `Node: ${data.label || data.id}`;
1695
+ navigator.clipboard.writeText(nodeText);
1696
+ }
1697
+ } else {
1698
+ const edgeText = `Edge: ${data.data.start_id_} -> ${data.data.end_id_}`;
1699
+ navigator.clipboard.writeText(edgeText);
1700
+ }
1701
+ onClose();
1702
+ };
1703
+ const handleHideNode = () => {
1704
+ if (elementType === "node") {
1705
+ const nodeId = data.id;
1706
+ devLog7("[ReaContextMenu] Hide node:", nodeId);
1707
+ useGraphExploration.getState().hideElements([nodeId]);
1708
+ }
1709
+ onClose();
1710
+ };
1711
+ const handleHideIncoming = () => {
1712
+ if (elementType === "node") {
1713
+ const nodeId = data.id;
1714
+ devLog7("[ReaContextMenu] Hide incoming edges:", nodeId);
1715
+ useGraphExploration.getState().hideNodeIncoming(nodeId);
1716
+ }
1717
+ onClose();
1718
+ };
1719
+ const handleHideOutgoing = () => {
1720
+ if (elementType === "node") {
1721
+ const nodeId = data.id;
1722
+ devLog7("[ReaContextMenu] Hide outgoing edges:", nodeId);
1723
+ useGraphExploration.getState().hideNodeOutgoing(nodeId);
1724
+ }
1725
+ onClose();
1726
+ };
1727
+ const handleExpand = () => {
1728
+ if (elementType === "node") {
1729
+ const nodeId = data.id;
1730
+ devLog7("[ReaContextMenu] Expand node:", nodeId);
1731
+ useGraphExploration.getState().expandNode(nodeId);
1732
+ }
1733
+ onClose();
1734
+ };
1735
+ const handleHideForwardPath = () => {
1736
+ if (elementType === "edge") {
1737
+ const edgeId = data.id;
1738
+ devLog7("[ReaContextMenu] Hide forward path from edge:", edgeId);
1739
+ useGraphExploration.getState().hideEdgeForwardPath(edgeId);
1740
+ }
1741
+ onClose();
1742
+ };
1743
+ const handleHideReversePath = () => {
1744
+ if (elementType === "edge") {
1745
+ const edgeId = data.id;
1746
+ devLog7("[ReaContextMenu] Hide reverse path from edge:", edgeId);
1747
+ useGraphExploration.getState().hideEdgeReversePath(edgeId);
1748
+ }
1749
+ onClose();
1750
+ };
1751
+ const handleFocusOnEdgeType = () => {
1752
+ if (elementType === "edge") {
1753
+ const edgeId = data.id;
1754
+ const edgeLabel = data.data?.label_ || "edge";
1755
+ devLog7("[ReaContextMenu] Focus on edge type:", edgeId, edgeLabel);
1756
+ useGraphExploration.getState().focusOnEdgeType(edgeId);
1757
+ }
1758
+ onClose();
1759
+ };
1760
+ const nodeMenuItems = [
1761
+ { label: "Focus", action: handleFocus },
1762
+ { label: "Copy", action: handleCopy },
1763
+ { label: "Hide Node", action: handleHideNode },
1764
+ { label: "Hide Incoming", action: handleHideIncoming },
1765
+ { label: "Hide Outgoing", action: handleHideOutgoing },
1766
+ { label: "Expand", action: handleExpand }
1767
+ ];
1768
+ const edgeMenuItems = [
1769
+ { label: "Focus", action: handleFocus },
1770
+ { label: "Focus Edge Type", action: handleFocusOnEdgeType },
1771
+ { label: "Copy", action: handleCopy },
1772
+ { label: "Hide Forward Path", action: handleHideForwardPath },
1773
+ { label: "Hide Reverse Path", action: handleHideReversePath }
1774
+ ];
1775
+ const menuItems = elementType === "node" ? nodeMenuItems : edgeMenuItems;
1776
+ return /* @__PURE__ */ jsx5("div", { className: "bg-white border border-gray-200 rounded-md shadow-lg py-1 min-w-48", children: menuItems.map((item, index) => /* @__PURE__ */ jsxs4(React2.Fragment, { children: [
1777
+ /* @__PURE__ */ jsx5(
1778
+ "button",
1779
+ {
1780
+ className: "w-full text-left px-3 py-2 text-sm hover:bg-gray-100 cursor-pointer",
1781
+ onClick: item.action,
1782
+ children: item.label
1783
+ }
1784
+ ),
1785
+ index === 1 && /* @__PURE__ */ jsx5("div", { className: "border-t border-gray-200 my-1" })
1786
+ ] }, item.label)) });
1787
+ }
1788
+ var ReaContextMenu_default = ReaContextMenu;
1789
+
1790
+ // src/components/rea/ReaGraph.tsx
1791
+ import { toast } from "sonner";
1792
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1793
+ function findElementInGraphData(elementId, elementType, nodes, edges) {
1794
+ if (elementType === "node") {
1795
+ const node = nodes.find((n) => n.id === elementId);
1796
+ if (node) return { element: node, actualElementType: "node" };
1797
+ const edge = edges.find((e) => e.id === elementId);
1798
+ if (edge) {
1799
+ devLog8("[ReGraph] ID mismatch: requested as node but found as edge", {
1800
+ elementId
1801
+ });
1802
+ return { element: edge, actualElementType: "edge" };
1803
+ }
1804
+ } else {
1805
+ const edge = edges.find((e) => e.id === elementId);
1806
+ if (edge) return { element: edge, actualElementType: "edge" };
1807
+ const node = nodes.find((n) => n.id === elementId);
1808
+ if (node) {
1809
+ devLog8("[ReGraph] ID mismatch: requested as edge but found as node", {
1810
+ elementId
1811
+ });
1812
+ return { element: node, actualElementType: "node" };
1813
+ }
1814
+ }
1815
+ return null;
1816
+ }
1817
+ function processFocusAction(focusAction, canvasRef, nodes, edges, setSelectedNodeIds, setSelectedEdge) {
1818
+ const { elementId, elementType, action } = focusAction;
1819
+ const found = findElementInGraphData(elementId, elementType, nodes, edges);
1820
+ if (!found) {
1821
+ devLog8("[ReGraph] Element not found in current Reagraph data", {
1822
+ elementId,
1823
+ elementType,
1824
+ availableNodes: nodes.length,
1825
+ availableEdges: edges.length,
1826
+ nodeIds: nodes.slice(0, 5).map((n) => n.id),
1827
+ edgeIds: edges.slice(0, 5).map((e) => e.id)
1828
+ });
1829
+ return false;
1830
+ }
1831
+ const { element, actualElementType } = found;
1832
+ devLog8("[ReGraph] Element found, focusing", {
1833
+ elementId,
1834
+ elementType: actualElementType
1835
+ });
1836
+ if (actualElementType === "node") {
1837
+ canvasRef.fitNodesInView([elementId]);
1838
+ } else {
1839
+ const edge = element;
1840
+ canvasRef.fitNodesInView([edge.source, edge.target]);
1841
+ }
1842
+ if (action === "focus-and-select") {
1843
+ if (actualElementType === "node") {
1844
+ setSelectedNodeIds([elementId]);
1845
+ } else {
1846
+ const edgeForFocus = edges.find((e) => e.id === elementId);
1847
+ setSelectedEdge(
1848
+ edgeForFocus ? edgeForFocus.data : null
1849
+ );
1850
+ }
1851
+ }
1852
+ return true;
1853
+ }
1854
+ function ReaGraph({ onCopyNode, extraToolbarContent }) {
1855
+ const isMountedRef = useRef(false);
1856
+ const { nodes, edges } = useReagraphData();
1857
+ const { normalized } = useLiveGraphData();
1858
+ useEffect2(() => {
1859
+ devLog8("[ReGraph] counts", { nodes: nodes.length, edges: edges.length });
1860
+ if (nodes.length > 0) devLog8("[ReGraph] sample node", nodes[0]);
1861
+ if (edges.length > 0) devLog8("[ReGraph] sample edge", edges[0]);
1862
+ }, [nodes, edges]);
1863
+ const canvas = useRef(null);
1864
+ const [hoveredNode, setHoveredNode] = useState3(
1865
+ void 0
1866
+ );
1867
+ const [layout, setLayout] = useState3("forceDirected2d");
1868
+ const [fullscreen, setFullscreen] = useState3(false);
1869
+ const [pathSelectionType, setPathSelectionType] = useState3("direct");
1870
+ const useGraphSelection = useWorkspaceSelection();
1871
+ const useGraphFilter2 = useWorkspaceGraphFilter();
1872
+ const useGraphExploration = useWorkspaceGraphExploration();
1873
+ const hiddenElementIds = useGraphExploration((s) => s.hiddenElementIds);
1874
+ const isFocusMode = useGraphExploration((s) => s.isFocusMode);
1875
+ const hasHiddenElements = hiddenElementIds.size > 0;
1876
+ const { setSelectedNodeIds, setSelectedEdge } = useGraphSelection(
1877
+ (s) => s
1878
+ );
1879
+ const lastDataChangeTime = useGraphFilter2((s) => s.lastDataChangeTime);
1880
+ const { clear: clearSelectionStore } = useGraphSelection.getState();
1881
+ const {
1882
+ selections,
1883
+ actives,
1884
+ onNodeClick,
1885
+ onCanvasClick,
1886
+ clearSelections,
1887
+ setSelections
1888
+ } = useSelection2({
1889
+ ref: canvas,
1890
+ nodes,
1891
+ edges,
1892
+ pathSelectionType,
1893
+ type: "multi"
1894
+ });
1895
+ useEffect2(() => {
1896
+ const unsubscribe = GraphFocus_default.subscribe((state) => {
1897
+ const focusAction = state.focusAction;
1898
+ if (!focusAction || !canvas.current) return;
1899
+ devLog8("[ReGraph] Processing focus action", {
1900
+ elementId: focusAction.elementId,
1901
+ elementType: focusAction.elementType,
1902
+ action: focusAction.action,
1903
+ timestamp: focusAction.timestamp
1904
+ });
1905
+ const handled = processFocusAction(
1906
+ focusAction,
1907
+ canvas.current,
1908
+ nodes,
1909
+ edges,
1910
+ setSelectedNodeIds,
1911
+ setSelectedEdge
1912
+ );
1913
+ if (handled) {
1914
+ GraphFocus_default.setState({ focusAction: null });
1915
+ }
1916
+ });
1917
+ return unsubscribe;
1918
+ }, [nodes, edges, setSelectedNodeIds, setSelectedEdge]);
1919
+ useEffect2(() => {
1920
+ if (!canvas.current) return;
1921
+ clearSelections();
1922
+ clearSelectionStore();
1923
+ }, [lastDataChangeTime, clearSelections, clearSelectionStore]);
1924
+ const fitToScreen = useCallback2(
1925
+ (reason) => {
1926
+ if (!isMountedRef.current) return;
1927
+ if (nodes.length === 0) return;
1928
+ requestAnimationFrame(() => {
1929
+ requestAnimationFrame(() => {
1930
+ if (!isMountedRef.current) return;
1931
+ const ref = canvas.current;
1932
+ if (!ref) return;
1933
+ try {
1934
+ ref.fitNodesInView();
1935
+ devLog8("[ReGraph] fitToScreen", {
1936
+ reason,
1937
+ nodeCount: nodes.length
1938
+ });
1939
+ } catch (e) {
1940
+ devLog8("[ReGraph] fitToScreen failed", {
1941
+ reason,
1942
+ error: e.message
1943
+ });
1944
+ }
1945
+ });
1946
+ });
1947
+ },
1948
+ [nodes.length]
1949
+ );
1950
+ useEffect2(() => {
1951
+ isMountedRef.current = true;
1952
+ fitToScreen("mount");
1953
+ const state = useGraphSelection.getState();
1954
+ if (state.selectedNodeIds.length || state.selectedEdge) {
1955
+ devLog8("[ReGraph] Initial sync of selection store", {
1956
+ selectedNodeIds: state.selectedNodeIds,
1957
+ selectedEdge: state.selectedEdge
1958
+ });
1959
+ if (state.selectedNodeIds.length > 0) {
1960
+ setSelections(state.selectedNodeIds);
1961
+ }
1962
+ if (state.selectedEdge) {
1963
+ setSelectedEdge(state.selectedEdge);
1964
+ }
1965
+ }
1966
+ return () => {
1967
+ isMountedRef.current = false;
1968
+ };
1969
+ }, [fitToScreen, setSelections, setSelectedEdge, useGraphSelection]);
1970
+ useEffect2(() => {
1971
+ if (isMountedRef.current) fitToScreen("counts-changed");
1972
+ }, [nodes.length, edges.length, fitToScreen]);
1973
+ function updateLayout(next) {
1974
+ setLayout(next);
1975
+ fitToScreen("layout-change");
1976
+ }
1977
+ function handleNodeClick(node) {
1978
+ setSelectedEdge(null);
1979
+ if (selections.length < 2 && onNodeClick) onNodeClick(node);
1980
+ }
1981
+ function handleEdgeClick(edge) {
1982
+ const startNode = normalized.nodesById[edge.data.start_id_ + ""];
1983
+ const endNode = normalized.nodesById[edge.data.end_id_ + ""];
1984
+ if (!startNode || !endNode) return;
1985
+ const startNodeId = String(startNode.id_);
1986
+ const endNodeId = String(endNode.id_);
1987
+ const currentSelectedIds = useGraphSelection.getState().selectedNodeIds;
1988
+ if (currentSelectedIds.length === 1) {
1989
+ const selectedNodeId = currentSelectedIds[0];
1990
+ if (selectedNodeId === startNodeId || selectedNodeId === endNodeId) {
1991
+ devLog8("[ReGraph] handleEdgeClick - node+edge selection", {
1992
+ edgeId: edge.id,
1993
+ selectedNode: selectedNodeId,
1994
+ direction: selectedNodeId === startNodeId ? "outgoing" : "incoming"
1995
+ });
1996
+ setSelectedEdge(edge.data);
1997
+ return;
1998
+ }
1999
+ }
2000
+ devLog8("[ReGraph] handleEdgeClick - default (both endpoints)", {
2001
+ edgeId: edge.id
2002
+ });
2003
+ setSelections([startNodeId, endNodeId]);
2004
+ setSelectedEdge(edge.data);
2005
+ }
2006
+ function handleCanvasClick(e) {
2007
+ clearSelections();
2008
+ setSelectedNodeIds([]);
2009
+ setSelectedEdge(null);
2010
+ if (onCanvasClick) onCanvasClick(e);
2011
+ }
2012
+ useEffect2(() => {
2013
+ if (!isMountedRef.current) return;
2014
+ if (selections.length > 0 && selections.length <= 2) {
2015
+ const selectedNodes = selections.map((id) => nodes.find((n) => n.id === id)).filter(Boolean);
2016
+ setSelectedNodeIds(selectedNodes.map((n) => n.id));
2017
+ }
2018
+ }, [selections, nodes]);
2019
+ useEffect2(() => {
2020
+ if (!isMountedRef.current) return;
2021
+ const currentSelectedIds = useGraphSelection.getState().selectedNodeIds;
2022
+ if (!currentSelectedIds.length) return;
2023
+ const stillPresentIds = currentSelectedIds.filter(
2024
+ (id) => nodes.find((n) => n.id === id)
2025
+ );
2026
+ if (stillPresentIds.length !== currentSelectedIds.length) {
2027
+ clearSelections();
2028
+ setSelectedNodeIds(stillPresentIds);
2029
+ if (stillPresentIds.length < 2) setSelectedEdge(null);
2030
+ }
2031
+ }, [
2032
+ nodes,
2033
+ clearSelections,
2034
+ setSelectedNodeIds,
2035
+ setSelectedEdge,
2036
+ useGraphSelection
2037
+ ]);
2038
+ useEffect2(() => {
2039
+ if (!isMountedRef.current) return;
2040
+ const currentSelectedIds = useGraphSelection.getState().selectedNodeIds;
2041
+ if (!currentSelectedIds.length) return;
2042
+ const stillPresentIds = currentSelectedIds.filter(
2043
+ (id) => nodes.find((n) => n.id === id)
2044
+ );
2045
+ if (stillPresentIds.length !== currentSelectedIds.length) {
2046
+ setSelectedNodeIds(stillPresentIds);
2047
+ if (stillPresentIds.length < 2) setSelectedEdge(null);
2048
+ }
2049
+ }, [nodes, setSelectedNodeIds, setSelectedEdge, useGraphSelection]);
2050
+ useEffect2(() => {
2051
+ if (!fullscreen) return;
2052
+ const prevOverflow = document.body.style.overflow;
2053
+ document.body.style.overflow = "hidden";
2054
+ fitToScreen("enter-fullscreen");
2055
+ const handleKey = (e) => {
2056
+ if (e.key === "Escape") setFullscreen(false);
2057
+ };
2058
+ window.addEventListener("keydown", handleKey);
2059
+ return () => {
2060
+ document.body.style.overflow = prevOverflow;
2061
+ window.removeEventListener("keydown", handleKey);
2062
+ };
2063
+ }, [fullscreen, fitToScreen]);
2064
+ useEffect2(() => {
2065
+ if (!fullscreen) return;
2066
+ const id = setTimeout(() => {
2067
+ if (canvas.current) {
2068
+ fitToScreen("fullscreen-resize-delay");
2069
+ }
2070
+ }, 50);
2071
+ return () => clearTimeout(id);
2072
+ }, [fullscreen, fitToScreen]);
2073
+ useEffect2(() => {
2074
+ if (!isMountedRef.current) return;
2075
+ const handleResize = () => {
2076
+ if (isMountedRef.current && canvas.current) {
2077
+ fitToScreen("window-resize");
2078
+ }
2079
+ };
2080
+ window.addEventListener("resize", handleResize);
2081
+ return () => {
2082
+ window.removeEventListener("resize", handleResize);
2083
+ };
2084
+ }, [fitToScreen]);
2085
+ const firstDataRef = useRef(true);
2086
+ useEffect2(() => {
2087
+ if (!isMountedRef.current) return;
2088
+ if (nodes.length > 0) {
2089
+ if (firstDataRef.current) {
2090
+ fitToScreen("first-data");
2091
+ firstDataRef.current = false;
2092
+ }
2093
+ } else {
2094
+ firstDataRef.current = true;
2095
+ }
2096
+ }, [nodes.length, fitToScreen]);
2097
+ if (nodes.length === 0) {
2098
+ devLog8("[ReGraph] empty state render");
2099
+ return /* @__PURE__ */ jsxs5(Card2, { className: "relative w-full h-full items-center justify-center text-slate-500 gap-2 shadow-xs p-4", children: [
2100
+ /* @__PURE__ */ jsx6("div", { children: "No data to display." }),
2101
+ /* @__PURE__ */ jsxs5("div", { children: [
2102
+ "Please",
2103
+ " ",
2104
+ /* @__PURE__ */ jsx6("span", { className: "font-semibold", children: "run a query, check filters, or add a node" }),
2105
+ " ",
2106
+ "to the workbench to populate the graph."
2107
+ ] })
2108
+ ] });
2109
+ }
2110
+ return /* @__PURE__ */ jsxs5(
2111
+ "div",
2112
+ {
2113
+ className: cn(
2114
+ "bg-white",
2115
+ fullscreen ? "fixed inset-0 z-50 w-screen h-screen p-0 m-0" : "relative w-full h-full"
2116
+ ),
2117
+ children: [
2118
+ /* @__PURE__ */ jsx6(Card2, { className: "h-full shadow-xs p-4", children: /* @__PURE__ */ jsx6(
2119
+ GraphCanvas,
2120
+ {
2121
+ ref: canvas,
2122
+ theme,
2123
+ nodes,
2124
+ edges,
2125
+ draggable: true,
2126
+ layoutType: layout,
2127
+ selections,
2128
+ actives,
2129
+ onCanvasClick: handleCanvasClick,
2130
+ onNodeClick: (e) => handleNodeClick(e),
2131
+ edgeArrowPosition: "end",
2132
+ edgeLabelPosition: "natural",
2133
+ labelType: "all",
2134
+ onNodePointerOver: setHoveredNode,
2135
+ onNodePointerOut: () => setHoveredNode(void 0),
2136
+ onEdgeClick: handleEdgeClick,
2137
+ contextMenu: ({ data, onClose }) => {
2138
+ const elementType = "source" in data ? "edge" : "node";
2139
+ devLog8("[ReGraph] Context menu opened", {
2140
+ elementId: data.id,
2141
+ elementType
2142
+ });
2143
+ return /* @__PURE__ */ jsx6(
2144
+ ReaContextMenu_default,
2145
+ {
2146
+ data,
2147
+ onClose,
2148
+ elementType,
2149
+ useGraphExploration,
2150
+ onCopyNode
2151
+ }
2152
+ );
2153
+ }
2154
+ }
2155
+ ) }),
2156
+ hoveredNode && /* @__PURE__ */ jsx6(NodeHoverInfo_default, { node: hoveredNode }),
2157
+ /* @__PURE__ */ jsx6(
2158
+ LayoutAndCameraControls_default,
2159
+ {
2160
+ fitToScreen,
2161
+ fullscreen,
2162
+ toggleFullscreen: () => setFullscreen((prev) => !prev),
2163
+ updateLayout,
2164
+ setPathSelectionType,
2165
+ hasHiddenElements,
2166
+ isFocusMode,
2167
+ onResetView: () => {
2168
+ useGraphExploration.getState().resetView();
2169
+ toast.success(
2170
+ isFocusMode ? "Focus mode cleared" : "Hidden elements restored"
2171
+ );
2172
+ },
2173
+ extraToolbarContent
2174
+ }
2175
+ )
2176
+ ]
2177
+ }
2178
+ );
2179
+ }
2180
+ var ReaGraph_default = ReaGraph;
2181
+
2182
+ // src/components/cyto/CytoscapeGraph.tsx
2183
+ import {
2184
+ useEffect as useEffect3,
2185
+ useRef as useRef2,
2186
+ useState as useState4,
2187
+ useCallback as useCallback3
2188
+ } from "react";
2189
+ import { devLog as devLog10, errorLog as errorLog2 } from "@petrarca/sonnet-core";
2190
+ import cytoscape from "cytoscape";
2191
+ import fcose from "cytoscape-fcose";
2192
+ import cola from "cytoscape-cola";
2193
+ import dagre from "cytoscape-dagre";
2194
+ import klay from "cytoscape-klay";
2195
+ import elk from "cytoscape-elk";
2196
+ import cise from "cytoscape-cise";
2197
+ import cxtmenu from "cytoscape-cxtmenu";
2198
+
2199
+ // src/components/cyto/CytoscapeControls.tsx
2200
+ import {
2201
+ Button as Button3,
2202
+ DropdownMenu as DropdownMenu3,
2203
+ DropdownMenuContent as DropdownMenuContent3,
2204
+ DropdownMenuItem as DropdownMenuItem3,
2205
+ DropdownMenuLabel as DropdownMenuLabel2,
2206
+ DropdownMenuSeparator as DropdownMenuSeparator2,
2207
+ DropdownMenuTrigger as DropdownMenuTrigger3,
2208
+ SimpleTooltip as SimpleTooltip3
2209
+ } from "@petrarca/sonnet-ui";
2210
+ import { LayoutPanelTop as LayoutPanelTop2, Monitor, Frame, Eye as Eye2, Minus } from "lucide-react";
2211
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2212
+ function ViewControls2({
2213
+ zoomIn,
2214
+ zoomOut,
2215
+ fullscreen,
2216
+ toggleFullscreen,
2217
+ fitToScreen,
2218
+ hasHiddenElements,
2219
+ isFocusMode,
2220
+ onResetView
2221
+ }) {
2222
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
2223
+ /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Zoom In", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: zoomIn, children: /* @__PURE__ */ jsx7("span", { style: { fontWeight: 600 }, children: "+" }) }) }),
2224
+ /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Zoom Out", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: zoomOut, children: /* @__PURE__ */ jsx7(Minus, {}) }) }),
2225
+ /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Fullscreen", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: toggleFullscreen, children: !fullscreen ? /* @__PURE__ */ jsx7(Monitor, {}) : /* @__PURE__ */ jsx7(Frame, {}) }) }),
2226
+ /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Fit to screen", side: "left", children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: fitToScreen, children: /* @__PURE__ */ jsx7(Frame, {}) }) }),
2227
+ (hasHiddenElements || isFocusMode) && onResetView && /* @__PURE__ */ jsx7(
2228
+ SimpleTooltip3,
2229
+ {
2230
+ label: isFocusMode ? "Reset Focus" : "Show All Hidden",
2231
+ side: "left",
2232
+ children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", onClick: onResetView, children: /* @__PURE__ */ jsx7(Eye2, {}) })
2233
+ }
2234
+ )
2235
+ ] });
2236
+ }
2237
+ var LAYOUT_GROUPS = [
2238
+ {
2239
+ label: "Force-directed Layouts",
2240
+ items: [
2241
+ { layout: "cola", name: "Cola (Best for Knowledge Graphs)" },
2242
+ { layout: "fcose", name: "F-CoSE (Large Graphs)" },
2243
+ { layout: "cise", name: "CiSE (Clustered Circles)" },
2244
+ { layout: "cose", name: "CoSE" }
2245
+ ]
2246
+ },
2247
+ {
2248
+ label: "Hierarchical Layouts",
2249
+ items: [
2250
+ { layout: "dagre", name: "Dagre (Left to Right)" },
2251
+ { layout: "klay", name: "KLay (Layered)" },
2252
+ { layout: "elk", name: "ELK (Layered)" },
2253
+ { layout: "breadthfirst", name: "Breadth-first" },
2254
+ { layout: "tree", name: "Tree (Top-Down)" }
2255
+ ]
2256
+ },
2257
+ {
2258
+ label: "Other Layouts",
2259
+ items: [
2260
+ { layout: "concentric", name: "Concentric" },
2261
+ { layout: "circle", name: "Circle" },
2262
+ { layout: "grid", name: "Grid" }
2263
+ ]
2264
+ }
2265
+ ];
2266
+ function LayoutDropdown2({ currentLayout, updateLayout }) {
2267
+ return /* @__PURE__ */ jsxs6(DropdownMenu3, { children: [
2268
+ /* @__PURE__ */ jsx7(SimpleTooltip3, { label: "Layout", side: "left", children: /* @__PURE__ */ jsx7(DropdownMenuTrigger3, { asChild: true, children: /* @__PURE__ */ jsx7(Button3, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx7(LayoutPanelTop2, {}) }) }) }),
2269
+ /* @__PURE__ */ jsx7(DropdownMenuContent3, { side: "left", align: "start", children: LAYOUT_GROUPS.map((group, groupIdx) => /* @__PURE__ */ jsxs6("div", { children: [
2270
+ groupIdx > 0 && /* @__PURE__ */ jsx7(DropdownMenuSeparator2, {}),
2271
+ /* @__PURE__ */ jsx7(DropdownMenuLabel2, { children: group.label }),
2272
+ group.items.map((item) => /* @__PURE__ */ jsx7(
2273
+ DropdownMenuItem3,
2274
+ {
2275
+ onClick: () => updateLayout(item.layout),
2276
+ className: currentLayout === item.layout ? "text-blue-600" : "",
2277
+ children: item.name
2278
+ },
2279
+ item.layout
2280
+ ))
2281
+ ] }, group.label)) })
2282
+ ] });
2283
+ }
2284
+ function CytoscapeControls({
2285
+ fitToScreen,
2286
+ hasHiddenElements,
2287
+ isFocusMode,
2288
+ updateLayout,
2289
+ zoomIn,
2290
+ zoomOut,
2291
+ currentLayout,
2292
+ fullscreen,
2293
+ toggleFullscreen,
2294
+ onResetView,
2295
+ extraToolbarContent
2296
+ }) {
2297
+ return /* @__PURE__ */ jsxs6("div", { className: "absolute right-2 top-2 flex flex-col border bg-white rounded-md shadow-sm z-10", children: [
2298
+ /* @__PURE__ */ jsx7(RendererDropdown, {}),
2299
+ /* @__PURE__ */ jsx7(
2300
+ ViewControls2,
2301
+ {
2302
+ zoomIn,
2303
+ zoomOut,
2304
+ fullscreen,
2305
+ toggleFullscreen,
2306
+ fitToScreen,
2307
+ hasHiddenElements,
2308
+ isFocusMode,
2309
+ onResetView
2310
+ }
2311
+ ),
2312
+ extraToolbarContent,
2313
+ /* @__PURE__ */ jsx7(
2314
+ LayoutDropdown2,
2315
+ {
2316
+ currentLayout,
2317
+ updateLayout
2318
+ }
2319
+ )
2320
+ ] });
2321
+ }
2322
+ var CytoscapeControls_default = CytoscapeControls;
2323
+
2324
+ // src/components/cyto/layoutPresets.ts
2325
+ function buildColaLayout(count) {
2326
+ return {
2327
+ name: "cola",
2328
+ // Always animate regardless of node count for better user experience
2329
+ animate: true,
2330
+ // Increase refresh rate for smoother animation
2331
+ refresh: 2,
2332
+ // Limit simulation time to prevent hanging with large graphs
2333
+ maxSimulationTime: 2e3,
2334
+ // Prevent grabbing during simulation for better performance
2335
+ ungrabifyWhileSimulating: true,
2336
+ fit: true,
2337
+ padding: 50,
2338
+ // Scale spacing based on node count for better distribution
2339
+ nodeSpacing: () => Math.max(10, 30 - Math.log(count) * 3),
2340
+ edgeLength: () => Math.max(80, 150 - Math.log(count) * 10),
2341
+ // Always avoid node overlap
2342
+ avoidOverlap: true,
2343
+ // Prevent infinite running
2344
+ infinite: false,
2345
+ // Add convergence threshold for faster completion with large graphs
2346
+ convergenceThreshold: 0.01,
2347
+ // Add randomize false to maintain consistent layouts
2348
+ randomize: false
2349
+ };
2350
+ }
2351
+ function buildDagreLayout(count) {
2352
+ return {
2353
+ name: "dagre",
2354
+ rankDir: "LR",
2355
+ // Left to right
2356
+ // Much more compact separation for tree-like hierarchical layouts
2357
+ rankSep: Math.min(80, 50 + Math.sqrt(count) * 1.5),
2358
+ // Further reduced
2359
+ nodeSep: Math.min(40, 25 + Math.sqrt(count) * 0.8),
2360
+ // Further reduced
2361
+ edgeSep: Math.min(10, 5 + Math.log(count) * 0.5),
2362
+ // Further reduced
2363
+ // Always animate for better user experience
2364
+ animate: true,
2365
+ fit: true,
2366
+ padding: 30
2367
+ // Reduced for more compact layout
2368
+ };
2369
+ }
2370
+ function buildTreeLayout(count) {
2371
+ return {
2372
+ name: "breadthfirst",
2373
+ fit: true,
2374
+ padding: 30,
2375
+ // Reduced for more compact layout
2376
+ animate: true,
2377
+ directed: true,
2378
+ spacingFactor: Math.min(0.9, 0.7 + 60 / (count + 50)),
2379
+ // Compact spacing: 0.7 to 0.9
2380
+ circle: false
2381
+ };
2382
+ }
2383
+ function buildKlayLayout(count) {
2384
+ return {
2385
+ name: "klay",
2386
+ nodeDimensionsIncludeLabels: true,
2387
+ fit: true,
2388
+ padding: 30,
2389
+ // Reduced for more compact layout
2390
+ animate: true,
2391
+ animationDuration: 500,
2392
+ klay: {
2393
+ direction: "RIGHT",
2394
+ spacing: Math.min(30, 12 + Math.sqrt(count) * 1.5),
2395
+ // Further reduced for much denser layout
2396
+ nodeLayering: "NETWORK_SIMPLEX",
2397
+ layoutHierarchy: true,
2398
+ // Increase thoroughness a bit for nicer layering on medium graphs
2399
+ thoroughness: count < 400 ? 30 : 10
2400
+ }
2401
+ };
2402
+ }
2403
+ function buildFcoseLayout(count) {
2404
+ return {
2405
+ name: "fcose",
2406
+ animate: true,
2407
+ animationDuration: 600,
2408
+ fit: true,
2409
+ padding: 50,
2410
+ // Scale edge length based on node count
2411
+ idealEdgeLength: Math.min(150, 80 + Math.sqrt(count) * 3),
2412
+ // Scale repulsion based on node count
2413
+ nodeRepulsion: Math.min(3e4, 4e3 + count * 50),
2414
+ // Scale separation based on node count
2415
+ nodeSeparation: Math.min(100, 50 + Math.sqrt(count) * 2),
2416
+ // Adjust gravity based on node count
2417
+ gravity: 0.3,
2418
+ // Never randomize for consistent layouts
2419
+ randomize: false,
2420
+ // Consistent energy for all graph sizes
2421
+ initialEnergyOnIncremental: 0.5,
2422
+ // Always use sampling for better performance
2423
+ samplingType: true,
2424
+ // Use default quality for best balance
2425
+ quality: "default",
2426
+ // Limit iterations for large graphs
2427
+ numIter: Math.min(2500, 1e3 + count * 5),
2428
+ // Use consistent cooling factor
2429
+ coolingFactor: 0.95,
2430
+ // Prevent infinite running
2431
+ infinite: false
2432
+ };
2433
+ }
2434
+ function buildCiseLayout(count, nodeData) {
2435
+ const labelToIds = {};
2436
+ nodeData.forEach((n) => {
2437
+ const label = n.data?.label_ || "Unknown";
2438
+ if (!labelToIds[label]) labelToIds[label] = [];
2439
+ labelToIds[label].push(n.id);
2440
+ });
2441
+ const clusters = Object.values(labelToIds).filter((arr) => arr.length > 0);
2442
+ return {
2443
+ name: "cise",
2444
+ animate: true,
2445
+ animationDuration: 600,
2446
+ clusters,
2447
+ nodeSeparation: Math.min(40, 20 + Math.sqrt(count)),
2448
+ idealInterClusterEdgeLengthCoefficient: 1.2,
2449
+ allowNodesInsideCircle: false,
2450
+ maxRatioOfNodesInsideCircle: 0.1,
2451
+ springCoeff: 0.45,
2452
+ nodeRepulsion: 4500,
2453
+ gravity: 0.25,
2454
+ gravityRange: 3.8,
2455
+ fit: true,
2456
+ padding: 50
2457
+ };
2458
+ }
2459
+ function buildElkLayout(count) {
2460
+ return {
2461
+ name: "elk",
2462
+ // elk algorithm options under `elk` key
2463
+ elk: {
2464
+ algorithm: "layered",
2465
+ "elk.direction": "RIGHT",
2466
+ "elk.layered.spacing.nodeNodeBetweenLayers": Math.min(
2467
+ 150,
2468
+ 60 + Math.sqrt(count) * 10
2469
+ ),
2470
+ "elk.spacing.nodeNode": Math.min(120, 40 + Math.sqrt(count) * 8),
2471
+ "elk.layered.crossingMinimization.semiInteractive": "true"
2472
+ },
2473
+ fit: true,
2474
+ padding: 50,
2475
+ animate: true,
2476
+ animationDuration: 500
2477
+ };
2478
+ }
2479
+ function buildCoseLayout(count) {
2480
+ return {
2481
+ name: "cose",
2482
+ // Always animate for better user experience
2483
+ animate: true,
2484
+ fit: true,
2485
+ padding: 50,
2486
+ // Scale overlap based on node count
2487
+ nodeOverlap: Math.min(30, 15 + Math.sqrt(count)),
2488
+ // Scale spacing based on node count
2489
+ componentSpacing: Math.min(150, 80 + Math.sqrt(count) * 2),
2490
+ // Increase refresh for smoother animation
2491
+ refresh: 10,
2492
+ // Scale parameters based on node count
2493
+ idealEdgeLength: () => Math.min(150, 80 + Math.sqrt(count) * 2),
2494
+ edgeElasticity: () => Math.min(150, 80 + Math.sqrt(count) * 2),
2495
+ nodeRepulsion: () => Math.min(6e5, 2e5 + count * 1e3),
2496
+ nestingFactor: 1.2,
2497
+ // Adjust gravity based on node count
2498
+ gravity: Math.min(100, 60 + Math.sqrt(count)),
2499
+ // Limit iterations for large graphs
2500
+ numIter: Math.min(2500, 1e3 + count * 5),
2501
+ // Use consistent cooling factor
2502
+ coolingFactor: 0.95
2503
+ };
2504
+ }
2505
+ function buildConcentricLayout(count) {
2506
+ return {
2507
+ name: "concentric",
2508
+ fit: true,
2509
+ padding: 50,
2510
+ // Always animate for better user experience
2511
+ animate: true,
2512
+ // Scale spacing based on node count
2513
+ minNodeSpacing: Math.min(60, 40 + Math.log(count) * 3),
2514
+ // Adjust level width based on node count
2515
+ levelWidth: () => Math.max(1, 3 - Math.log(count) * 0.3),
2516
+ // Use degree for concentric layout
2517
+ concentric: (node) => node.degree(),
2518
+ // Start from outside for better distribution
2519
+ startAngle: Math.PI * 1.5
2520
+ };
2521
+ }
2522
+ function buildBreadthfirstLayout(count) {
2523
+ return {
2524
+ name: "breadthfirst",
2525
+ fit: true,
2526
+ padding: 50,
2527
+ // Always animate for better user experience
2528
+ animate: true,
2529
+ directed: true,
2530
+ // Scale spacing based on node count
2531
+ spacingFactor: Math.min(1.8, 1.2 + 100 / (count + 50)),
2532
+ // Add circle option for better distribution with smaller graphs
2533
+ circle: count < 100
2534
+ };
2535
+ }
2536
+ function buildCircleLayout(count) {
2537
+ return {
2538
+ name: "circle",
2539
+ fit: true,
2540
+ padding: 50,
2541
+ // Always animate for better user experience
2542
+ animate: true,
2543
+ // Scale radius based on node count
2544
+ radius: Math.min(500, count * 3 + 50),
2545
+ // Add startAngle for consistent layout
2546
+ startAngle: Math.PI / 2
2547
+ };
2548
+ }
2549
+ function buildGridLayout(count) {
2550
+ const approxCols = Math.ceil(Math.sqrt(count * 1.5));
2551
+ const cols = Math.max(approxCols, 1);
2552
+ const rows = Math.max(Math.ceil(count / cols), 1);
2553
+ return {
2554
+ name: "grid",
2555
+ fit: true,
2556
+ padding: 40,
2557
+ avoidOverlap: true,
2558
+ // Extra space around each node beyond actual dimensions
2559
+ avoidOverlapPadding: Math.min(80, 20 + Math.log(count + 1) * 10),
2560
+ // Do NOT condense; we want uniform padding for labels
2561
+ condense: false,
2562
+ animate: true,
2563
+ animationDuration: 400,
2564
+ rows,
2565
+ cols
2566
+ };
2567
+ }
2568
+ var layoutFactories = {
2569
+ cola: buildColaLayout,
2570
+ dagre: buildDagreLayout,
2571
+ tree: buildTreeLayout,
2572
+ klay: buildKlayLayout,
2573
+ fcose: buildFcoseLayout,
2574
+ cise: buildCiseLayout,
2575
+ elk: buildElkLayout,
2576
+ cose: buildCoseLayout,
2577
+ concentric: buildConcentricLayout,
2578
+ breadthfirst: buildBreadthfirstLayout,
2579
+ circle: buildCircleLayout,
2580
+ grid: buildGridLayout
2581
+ };
2582
+ function getLayoutPreset(options) {
2583
+ let { layoutName } = options;
2584
+ const { count, nodeData = [] } = options;
2585
+ if (layoutName === "auto") {
2586
+ layoutName = "cola";
2587
+ }
2588
+ const factory = layoutFactories[layoutName] ?? layoutFactories.grid;
2589
+ return factory(count, nodeData);
2590
+ }
2591
+
2592
+ // src/components/cyto/contextMenuConfig.ts
2593
+ import { devLog as devLog9, errorLog } from "@petrarca/sonnet-core";
2594
+ import { toast as toast2 } from "sonner";
2595
+ var createNodeContextMenu = ({
2596
+ cy,
2597
+ useGraphExploration,
2598
+ onCopyNode
2599
+ }) => {
2600
+ devLog9("[CxtMenu] Initializing node context menu");
2601
+ return cy.cxtmenu({
2602
+ selector: "node",
2603
+ commands: [
2604
+ {
2605
+ content: "Focus",
2606
+ select: (ele) => {
2607
+ const nodeId = ele.id();
2608
+ devLog9("[CxtMenu] Focus on node and neighborhood:", nodeId);
2609
+ useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId);
2610
+ }
2611
+ },
2612
+ {
2613
+ content: "Copy",
2614
+ select: (ele) => {
2615
+ devLog9("[CxtMenu] Copy node to clipboard");
2616
+ const visualGraph = ele.data("visualGraph");
2617
+ if (visualGraph) {
2618
+ onCopyNode?.(visualGraph);
2619
+ } else {
2620
+ errorLog("[CxtMenu] visualGraph not found in element data");
2621
+ }
2622
+ }
2623
+ },
2624
+ {
2625
+ content: "Hide Node",
2626
+ select: (ele) => {
2627
+ const nodeId = ele.id();
2628
+ devLog9("[CxtMenu] Hide node:", nodeId);
2629
+ useGraphExploration.getState().hideElements([nodeId]);
2630
+ }
2631
+ },
2632
+ {
2633
+ content: "Hide Incoming",
2634
+ select: (ele) => {
2635
+ const nodeId = ele.id();
2636
+ devLog9("[CxtMenu] Hide incoming (predecessors) of node:", nodeId);
2637
+ useGraphExploration.getState().hideNodeIncoming(nodeId);
2638
+ }
2639
+ },
2640
+ {
2641
+ content: "Hide Outgoing",
2642
+ select: (ele) => {
2643
+ const nodeId = ele.id();
2644
+ devLog9("[CxtMenu] Hide outgoing (successors) of node:", nodeId);
2645
+ useGraphExploration.getState().hideNodeOutgoing(nodeId);
2646
+ }
2647
+ },
2648
+ {
2649
+ content: "Expand",
2650
+ select: async (ele) => {
2651
+ const nodeId = ele.id();
2652
+ devLog9("[CxtMenu] Expanding node:", nodeId);
2653
+ try {
2654
+ await useGraphExploration.getState().expandNode(nodeId);
2655
+ devLog9("[CxtMenu] Expansion complete");
2656
+ } catch (error) {
2657
+ errorLog("[CxtMenu] Expansion failed:", error);
2658
+ toast2.error("Failed to expand node");
2659
+ }
2660
+ }
2661
+ }
2662
+ ],
2663
+ menuRadius: 60,
2664
+ // Increased for more items (default is 100)
2665
+ fillColor: "rgba(0, 0, 0, 0.75)",
2666
+ activeFillColor: "rgba(59, 130, 246, 0.75)",
2667
+ // blue-500
2668
+ activePadding: 10,
2669
+ indicatorSize: 12,
2670
+ separatorWidth: 2,
2671
+ spotlightPadding: 2,
2672
+ adaptativeNodeSpotlightRadius: true,
2673
+ minSpotlightRadius: 12,
2674
+ maxSpotlightRadius: 20,
2675
+ openMenuEvents: "cxttapstart taphold",
2676
+ itemColor: "white",
2677
+ itemTextShadowColor: "transparent",
2678
+ zIndex: 9999,
2679
+ atMouse: false
2680
+ });
2681
+ };
2682
+ var createEdgeContextMenu = ({
2683
+ cy,
2684
+ useGraphExploration
2685
+ }) => {
2686
+ return cy.cxtmenu({
2687
+ selector: "edge",
2688
+ commands: [
2689
+ {
2690
+ content: "Focus",
2691
+ select: (ele) => {
2692
+ const edgeId = ele.id();
2693
+ devLog9(
2694
+ "[CxtMenu] Focus on edge (auto-direction based on selection):",
2695
+ edgeId
2696
+ );
2697
+ useGraphExploration.getState().focusEdge(edgeId);
2698
+ }
2699
+ },
2700
+ {
2701
+ content: "Focus Edge Type",
2702
+ select: (ele) => {
2703
+ const edgeId = ele.id();
2704
+ devLog9("[CxtMenu] Focus on edge type:", edgeId);
2705
+ useGraphExploration.getState().focusOnEdgeType(edgeId);
2706
+ }
2707
+ },
2708
+ {
2709
+ content: "Hide Forward Path",
2710
+ select: (ele) => {
2711
+ const edgeId = ele.id();
2712
+ devLog9(
2713
+ "[CxtMenu] Hide forward path (edge + target + downstream):",
2714
+ edgeId
2715
+ );
2716
+ useGraphExploration.getState().hideEdgeForwardPath(edgeId);
2717
+ }
2718
+ },
2719
+ {
2720
+ content: "Hide Reverse Path",
2721
+ select: (ele) => {
2722
+ const edgeId = ele.id();
2723
+ devLog9(
2724
+ "[CxtMenu] Hide reverse path (edge + source + upstream):",
2725
+ edgeId
2726
+ );
2727
+ useGraphExploration.getState().hideEdgeReversePath(edgeId);
2728
+ }
2729
+ }
2730
+ ],
2731
+ menuRadius: 90,
2732
+ // Increased for more items
2733
+ fillColor: "rgba(0, 0, 0, 0.75)",
2734
+ activeFillColor: "rgba(239, 68, 68, 0.75)",
2735
+ // red-500
2736
+ activePadding: 10,
2737
+ indicatorSize: 12,
2738
+ separatorWidth: 2,
2739
+ spotlightPadding: 2,
2740
+ openMenuEvents: "cxttapstart taphold",
2741
+ itemColor: "white",
2742
+ itemTextShadowColor: "transparent",
2743
+ zIndex: 9999,
2744
+ atMouse: false
2745
+ });
2746
+ };
2747
+
2748
+ // src/components/cyto/CytoscapeGraph.tsx
2749
+ import { toast as toast3 } from "sonner";
2750
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2751
+ try {
2752
+ cytoscape.use(fcose);
2753
+ cytoscape.use(cola);
2754
+ cytoscape.use(dagre);
2755
+ cytoscape.use(klay);
2756
+ cytoscape.use(elk);
2757
+ cytoscape.use(cise);
2758
+ cxtmenu(cytoscape);
2759
+ } catch {
2760
+ }
2761
+ var ALLOWED_LAYOUTS = [
2762
+ "auto",
2763
+ "fcose",
2764
+ "cola",
2765
+ "dagre",
2766
+ "klay",
2767
+ "elk",
2768
+ "cise",
2769
+ "grid",
2770
+ "cose",
2771
+ "concentric",
2772
+ "breadthfirst",
2773
+ "tree",
2774
+ "circle",
2775
+ "random"
2776
+ ];
2777
+ function classifyUpdate({
2778
+ cy,
2779
+ currentIds,
2780
+ elementIdsRef,
2781
+ lastLayoutRef,
2782
+ lastFitPaddingRef,
2783
+ currentLayout,
2784
+ fitPadding,
2785
+ nodeCount,
2786
+ edgeCount
2787
+ }) {
2788
+ const paddingChanged = fitPadding !== lastFitPaddingRef.current;
2789
+ const cyHasElements = cy.nodes().length === nodeCount && cy.edges().length === edgeCount;
2790
+ const layoutChanged = currentLayout !== lastLayoutRef.current;
2791
+ if (paddingChanged && currentIds === elementIdsRef.current && cyHasElements && !layoutChanged) {
2792
+ return "refit";
2793
+ }
2794
+ if (currentIds === elementIdsRef.current && cyHasElements && !layoutChanged) {
2795
+ return "skip";
2796
+ }
2797
+ return "update";
2798
+ }
2799
+ function applyLayout({
2800
+ cy,
2801
+ chosen,
2802
+ nodeCount,
2803
+ nodesForLayout,
2804
+ fitPadding
2805
+ }) {
2806
+ const layoutOpts = getLayoutPreset({
2807
+ layoutName: chosen,
2808
+ count: nodeCount,
2809
+ nodeData: nodesForLayout
2810
+ });
2811
+ if (chosen === "tree") {
2812
+ const cyNodes = cy.nodes();
2813
+ const rootCandidates = cyNodes.filter((n) => n.indegree(false) === 0);
2814
+ if (rootCandidates.length > 0) {
2815
+ layoutOpts.roots = rootCandidates;
2816
+ } else if (cyNodes.length) {
2817
+ layoutOpts.roots = cyNodes[0];
2818
+ }
2819
+ }
2820
+ const getPreset = (name) => getLayoutPreset({
2821
+ layoutName: name,
2822
+ count: nodeCount,
2823
+ nodeData: nodesForLayout
2824
+ });
2825
+ try {
2826
+ cy.layout(layoutOpts).run();
2827
+ } catch (e) {
2828
+ errorLog2("Layout error, falling back to dagre:", e);
2829
+ if (chosen === "klay") {
2830
+ try {
2831
+ cy.layout(getPreset("dagre")).run();
2832
+ } catch (inner) {
2833
+ errorLog2("Fallback layout also failed, using grid:", inner);
2834
+ cy.layout(getPreset("grid")).run();
2835
+ }
2836
+ } else {
2837
+ cy.layout(getPreset("grid")).run();
2838
+ }
2839
+ }
2840
+ if (!layoutOpts.fit) {
2841
+ cy.fit(void 0, fitPadding);
2842
+ }
2843
+ }
2844
+ function processPendingFocus({
2845
+ cy,
2846
+ pendingFocus,
2847
+ edgesRef,
2848
+ setSelectedNodeIdsStore,
2849
+ setSelectedEdgeStore,
2850
+ selectedNodeIdsRef,
2851
+ selectedEdgeRef,
2852
+ pendingFocusActionRef
2853
+ }) {
2854
+ devLog10("[CytoscapeGraph] Processing pending focus action", {
2855
+ elementId: pendingFocus.elementId,
2856
+ elementType: pendingFocus.elementType
2857
+ });
2858
+ setTimeout(() => {
2859
+ const element = cy.$id(pendingFocus.elementId);
2860
+ if (element.length === 0) {
2861
+ devLog10(
2862
+ "[CytoscapeGraph] Focus requested but element not found after graph update",
2863
+ {
2864
+ elementId: pendingFocus.elementId,
2865
+ elementType: pendingFocus.elementType
2866
+ }
2867
+ );
2868
+ pendingFocusActionRef.current = null;
2869
+ GraphFocus_default.setState({ focusAction: null });
2870
+ return;
2871
+ }
2872
+ cy.animate(
2873
+ { zoom: 2, center: { eles: element } },
2874
+ { duration: 500, easing: "ease-in-out-cubic" }
2875
+ );
2876
+ if (pendingFocus.action === "focus-and-select") {
2877
+ selectFocusedElement({
2878
+ cy,
2879
+ pendingFocus,
2880
+ element,
2881
+ edgesRef,
2882
+ setSelectedNodeIdsStore,
2883
+ setSelectedEdgeStore,
2884
+ selectedNodeIdsRef,
2885
+ selectedEdgeRef
2886
+ });
2887
+ }
2888
+ pendingFocusActionRef.current = null;
2889
+ GraphFocus_default.setState({ focusAction: null });
2890
+ }, 100);
2891
+ }
2892
+ function selectFocusedElement({
2893
+ cy,
2894
+ pendingFocus,
2895
+ element,
2896
+ edgesRef,
2897
+ setSelectedNodeIdsStore,
2898
+ setSelectedEdgeStore,
2899
+ selectedNodeIdsRef,
2900
+ selectedEdgeRef
2901
+ }) {
2902
+ if (pendingFocus.elementType === "node") {
2903
+ cy.batch(() => {
2904
+ cy.elements().unselect();
2905
+ element.select();
2906
+ });
2907
+ setSelectedNodeIdsStore([pendingFocus.elementId]);
2908
+ selectedNodeIdsRef.current = [pendingFocus.elementId];
2909
+ setSelectedEdgeStore(null);
2910
+ selectedEdgeRef.current = null;
2911
+ } else {
2912
+ const edgeObj = edgesRef.current.find(
2913
+ (e) => String(e.id_) === pendingFocus.elementId
2914
+ );
2915
+ if (edgeObj) {
2916
+ const sourceId = String(edgeObj.start_id_);
2917
+ const targetId = String(edgeObj.end_id_);
2918
+ cy.batch(() => {
2919
+ cy.elements().unselect();
2920
+ cy.$id(sourceId).select();
2921
+ cy.$id(targetId).select();
2922
+ element.select();
2923
+ });
2924
+ setSelectedNodeIdsStore([sourceId, targetId]);
2925
+ selectedNodeIdsRef.current = [sourceId, targetId];
2926
+ setSelectedEdgeStore(edgeObj);
2927
+ selectedEdgeRef.current = edgeObj;
2928
+ }
2929
+ }
2930
+ }
2931
+ function handleFocusStateChange({
2932
+ focusAction,
2933
+ cyRef,
2934
+ edgesRef,
2935
+ selectedNodeIdsRef,
2936
+ selectedEdgeRef,
2937
+ setSelectedNodeIdsStore,
2938
+ setSelectedEdgeStore,
2939
+ pendingFocusActionRef,
2940
+ baseNormalized
2941
+ }) {
2942
+ if (!focusAction || !cyRef.current) return;
2943
+ devLog10("[CytoscapeGraph] Processing focus action", {
2944
+ elementId: focusAction.elementId,
2945
+ timestamp: focusAction.timestamp
2946
+ });
2947
+ const cy = cyRef.current;
2948
+ const elementId = focusAction.elementId;
2949
+ const element = cy.$id(elementId);
2950
+ if (element.length > 0) {
2951
+ handleImmediateFocus({
2952
+ cy,
2953
+ element,
2954
+ focusAction,
2955
+ edgesRef,
2956
+ selectedNodeIdsRef,
2957
+ selectedEdgeRef,
2958
+ setSelectedNodeIdsStore,
2959
+ setSelectedEdgeStore
2960
+ });
2961
+ return;
2962
+ }
2963
+ devLog10("[CytoscapeGraph] Element not visible, setting up override", {
2964
+ elementId,
2965
+ elementType: focusAction.elementType
2966
+ });
2967
+ pendingFocusActionRef.current = focusAction;
2968
+ GraphFocus_default.setState((state) => {
2969
+ const overrides = new Set(state.overrideElementIds);
2970
+ overrides.add(elementId);
2971
+ if (focusAction.elementType === "edge") {
2972
+ let edgeObj = edgesRef.current.find((e) => String(e.id_) === elementId);
2973
+ if (!edgeObj) {
2974
+ edgeObj = baseNormalized.edgesById[elementId];
2975
+ }
2976
+ if (edgeObj) {
2977
+ overrides.add(String(edgeObj.start_id_));
2978
+ overrides.add(String(edgeObj.end_id_));
2979
+ devLog10("[CytoscapeGraph] Added edge and endpoints to overrides", {
2980
+ edgeId: elementId,
2981
+ startId: edgeObj.start_id_,
2982
+ endId: edgeObj.end_id_
2983
+ });
2984
+ }
2985
+ } else {
2986
+ devLog10("[CytoscapeGraph] Added node to overrides", { nodeId: elementId });
2987
+ }
2988
+ return { overrideElementIds: overrides };
2989
+ });
2990
+ }
2991
+ function handleImmediateFocus({
2992
+ cy,
2993
+ element,
2994
+ focusAction,
2995
+ edgesRef,
2996
+ selectedNodeIdsRef,
2997
+ selectedEdgeRef,
2998
+ setSelectedNodeIdsStore,
2999
+ setSelectedEdgeStore
3000
+ }) {
3001
+ const elementId = focusAction.elementId;
3002
+ devLog10("[CytoscapeGraph] Element already visible, focusing immediately", {
3003
+ elementId,
3004
+ elementType: focusAction.elementType
3005
+ });
3006
+ cy.animate(
3007
+ { zoom: 2, center: { eles: element } },
3008
+ { duration: 500, easing: "ease-in-out-cubic" }
3009
+ );
3010
+ if (focusAction.action === "focus-and-select") {
3011
+ selectFocusedElement({
3012
+ cy,
3013
+ pendingFocus: focusAction,
3014
+ element,
3015
+ edgesRef,
3016
+ setSelectedNodeIdsStore,
3017
+ setSelectedEdgeStore,
3018
+ selectedNodeIdsRef,
3019
+ selectedEdgeRef
3020
+ });
3021
+ }
3022
+ GraphFocus_default.setState({ focusAction: null });
3023
+ }
3024
+ function computeGraphViewState({
3025
+ hasNodes,
3026
+ hasHiddenElements,
3027
+ isFocusMode,
3028
+ fullscreen
3029
+ }) {
3030
+ const hasExplorationState = hasHiddenElements || isFocusMode;
3031
+ return {
3032
+ showEmptyMessage: !hasNodes && !hasExplorationState,
3033
+ showFilteredMessage: !hasNodes && hasExplorationState,
3034
+ showControls: hasNodes || hasExplorationState,
3035
+ outerClass: fullscreen ? "fixed inset-0 z-50 w-screen h-screen bg-white flex flex-col" : "w-full h-full flex flex-col relative",
3036
+ innerClass: fullscreen ? "flex-1 relative overflow-hidden bg-white" : "flex-1 relative border border-gray-300 rounded-md overflow-hidden bg-white"
3037
+ };
3038
+ }
3039
+ function useFullscreen(fitToScreen, cyRef) {
3040
+ const [fullscreen, setFullscreen] = useState4(false);
3041
+ useEffect3(() => {
3042
+ if (fullscreen) {
3043
+ const prevOverflow = document.body.style.overflow;
3044
+ document.body.style.overflow = "hidden";
3045
+ fitToScreen();
3046
+ const handleKey = (e) => {
3047
+ if (e.key === "Escape") setFullscreen(false);
3048
+ };
3049
+ window.addEventListener("keydown", handleKey);
3050
+ return () => {
3051
+ document.body.style.overflow = prevOverflow;
3052
+ window.removeEventListener("keydown", handleKey);
3053
+ };
3054
+ }
3055
+ }, [fullscreen, fitToScreen]);
3056
+ useEffect3(() => {
3057
+ if (!fullscreen) return;
3058
+ const id = setTimeout(() => {
3059
+ if (cyRef.current) {
3060
+ cyRef.current.resize();
3061
+ fitToScreen();
3062
+ }
3063
+ }, 50);
3064
+ return () => clearTimeout(id);
3065
+ }, [fullscreen, fitToScreen, cyRef]);
3066
+ return {
3067
+ fullscreen,
3068
+ setFullscreen,
3069
+ toggleFullscreen: useCallback3(() => setFullscreen((f) => !f), [])
3070
+ };
3071
+ }
3072
+ function setupCytoscapeEventHandlers({
3073
+ cy,
3074
+ edgesRef,
3075
+ selectedNodeIdsRef,
3076
+ selectedEdgeRef,
3077
+ setSelectedNodeIdsStore,
3078
+ setSelectedEdgeStore,
3079
+ clearSelectionStore,
3080
+ useGraphExploration,
3081
+ onCopyNode
3082
+ }) {
3083
+ const updateSelectedNodeIds = (nodeIds) => {
3084
+ setSelectedNodeIdsStore(nodeIds);
3085
+ selectedNodeIdsRef.current = nodeIds;
3086
+ };
3087
+ const updateSelectedEdge = (edgeToSet) => {
3088
+ setSelectedEdgeStore(edgeToSet);
3089
+ selectedEdgeRef.current = edgeToSet;
3090
+ };
3091
+ const clearAll = () => {
3092
+ cy.batch(() => {
3093
+ cy.elements().unselect();
3094
+ });
3095
+ clearSelectionStore();
3096
+ selectedNodeIdsRef.current = [];
3097
+ selectedEdgeRef.current = null;
3098
+ };
3099
+ cy.on("select", "node", (evt) => {
3100
+ const nodeEle = evt.target;
3101
+ const evtCy = evt.cy;
3102
+ const id = nodeEle.id();
3103
+ devLog10("[CytoscapeGraph] node selected", { id });
3104
+ if (selectedEdgeRef.current) {
3105
+ devLog10("[CytoscapeGraph] clearing edge due to node selection", {
3106
+ edgeId: selectedEdgeRef.current.id_
3107
+ });
3108
+ updateSelectedEdge(null);
3109
+ }
3110
+ let selectedIds = evtCy.$("node:selected").map((e) => e.id());
3111
+ if (selectedIds.length > 2) {
3112
+ const currentOrder = [...selectedNodeIdsRef.current];
3113
+ const updatedOrder = [...currentOrder.filter((cid) => cid !== id), id];
3114
+ const keep = updatedOrder.slice(-2);
3115
+ evtCy.batch(() => {
3116
+ evtCy.$("node:selected").unselect();
3117
+ keep.forEach((kid) => evtCy.$id(kid).select());
3118
+ });
3119
+ selectedIds = keep;
3120
+ devLog10("[CytoscapeGraph] enforced max 2 after select", { keep });
3121
+ }
3122
+ updateSelectedNodeIds(selectedIds);
3123
+ });
3124
+ cy.on("unselect", "node", (evt) => {
3125
+ devLog10("[CytoscapeGraph] node unselected", { id: evt.target.id() });
3126
+ const selectedIds = evt.cy.$("node:selected").map((e) => e.id());
3127
+ updateSelectedNodeIds(selectedIds);
3128
+ });
3129
+ cy.on("tap", "edge", (evt) => {
3130
+ const edgeEle = evt.target;
3131
+ const edgeId = edgeEle.id();
3132
+ const edgeObj = edgesRef.current.find((e) => String(e.id_) === edgeId);
3133
+ if (!edgeObj) return;
3134
+ const sourceId = edgeEle.data("source");
3135
+ const targetId = edgeEle.data("target");
3136
+ const evtCy = evt.cy;
3137
+ const currentlySelectedNodes = evtCy.$("node:selected").toArray();
3138
+ if (currentlySelectedNodes.length === 1) {
3139
+ const selectedNodeId = currentlySelectedNodes[0].id();
3140
+ if (selectedNodeId === sourceId || selectedNodeId === targetId) {
3141
+ devLog10("[CytoscapeGraph] tap edge - node+edge selection", {
3142
+ edgeId,
3143
+ selectedNode: selectedNodeId,
3144
+ direction: selectedNodeId === sourceId ? "outgoing" : "incoming"
3145
+ });
3146
+ evtCy.batch(() => {
3147
+ evtCy.elements().unselect();
3148
+ evtCy.$id(selectedNodeId).select();
3149
+ edgeEle.select();
3150
+ });
3151
+ updateSelectedNodeIds([selectedNodeId]);
3152
+ updateSelectedEdge(edgeObj);
3153
+ return;
3154
+ }
3155
+ }
3156
+ devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId });
3157
+ evtCy.batch(() => {
3158
+ evtCy.elements().unselect();
3159
+ evtCy.$id(sourceId).select();
3160
+ evtCy.$id(targetId).select();
3161
+ edgeEle.select();
3162
+ });
3163
+ updateSelectedNodeIds([sourceId, targetId]);
3164
+ updateSelectedEdge(edgeObj);
3165
+ });
3166
+ cy.on("tap", (evt) => {
3167
+ if (evt.target === cy) clearAll();
3168
+ });
3169
+ createNodeContextMenu({ cy, useGraphExploration, onCopyNode });
3170
+ createEdgeContextMenu({ cy, useGraphExploration });
3171
+ }
3172
+ function CytoscapeGraph({
3173
+ layout: initialLayout = "auto",
3174
+ fitPadding = 30,
3175
+ onCopyNode,
3176
+ extraToolbarContent
3177
+ }) {
3178
+ const containerRef = useRef2(null);
3179
+ const cyRef = useRef2(null);
3180
+ const isInitializedRef = useRef2(false);
3181
+ const [currentLayout, setCurrentLayout] = useState4(initialLayout);
3182
+ const nodesRef = useRef2([]);
3183
+ const edgesRef = useRef2([]);
3184
+ const pendingFocusActionRef = useRef2(null);
3185
+ const cytoData = useCytoscapeData();
3186
+ const live = useLiveGraphData();
3187
+ const nodes = live.nodes;
3188
+ const edges = live.edges;
3189
+ const baseNormalized = live.baseNormalized;
3190
+ const useGraphFilter2 = useWorkspaceGraphFilter();
3191
+ const useGraphExploration = useWorkspaceGraphExploration();
3192
+ const useSelection3 = useWorkspaceSelection();
3193
+ const lastDataChangeTime = useGraphFilter2((s) => s.lastDataChangeTime);
3194
+ const resetView = useGraphExploration((s) => s.resetView);
3195
+ const hasHiddenElements = useGraphExploration(
3196
+ (s) => s.hiddenElementIds.size > 0
3197
+ );
3198
+ const isFocusMode = useGraphExploration((s) => s.isFocusMode);
3199
+ const {
3200
+ setSelectedNodeIds: setSelectedNodeIdsStore,
3201
+ setSelectedEdge: setSelectedEdgeStore,
3202
+ clear: clearSelectionStore
3203
+ } = useSelection3.getState();
3204
+ const selectedNodeIdsRef = useRef2(
3205
+ useSelection3.getState().selectedNodeIds
3206
+ );
3207
+ const selectedEdgeRef = useRef2(
3208
+ useSelection3.getState().selectedEdge
3209
+ );
3210
+ useEffect3(() => {
3211
+ const state = useSelection3.getState();
3212
+ selectedNodeIdsRef.current = state.selectedNodeIds;
3213
+ selectedEdgeRef.current = state.selectedEdge;
3214
+ const cy = cyRef.current;
3215
+ if (cy && (state.selectedNodeIds.length || state.selectedEdge)) {
3216
+ cy.batch(() => {
3217
+ cy.elements().unselect();
3218
+ state.selectedNodeIds.forEach((id) => cy.$id(id).select());
3219
+ if (state.selectedEdge) {
3220
+ const edge = cy.$id(String(state.selectedEdge.id_));
3221
+ if (edge) {
3222
+ edge.select();
3223
+ cy.$id(edge.data("source")).select();
3224
+ cy.$id(edge.data("target")).select();
3225
+ }
3226
+ }
3227
+ });
3228
+ }
3229
+ }, [useSelection3]);
3230
+ useEffect3(() => {
3231
+ nodesRef.current = nodes;
3232
+ }, [nodes]);
3233
+ useEffect3(() => {
3234
+ edgesRef.current = edges;
3235
+ }, [edges]);
3236
+ useEffect3(() => {
3237
+ if (!containerRef.current || cyRef.current) return;
3238
+ isInitializedRef.current = true;
3239
+ cyRef.current = cytoscape({
3240
+ container: containerRef.current,
3241
+ elements: [],
3242
+ selectionType: "additive",
3243
+ // allow clicking multiple nodes without modifier
3244
+ style: [
3245
+ // Base node style with data mappers for dynamic properties
3246
+ {
3247
+ selector: "node",
3248
+ style: {
3249
+ "background-color": "data(color)",
3250
+ // Read color from element data
3251
+ "background-opacity": 0.9,
3252
+ width: "data(size)",
3253
+ // Read size from element data
3254
+ height: "data(size)",
3255
+ shape: "ellipse",
3256
+ label: "data(label)",
3257
+ // Dark gray text for readability on subtle gray background
3258
+ color: "#374151",
3259
+ "font-size": "11px",
3260
+ // Increased from 10px
3261
+ "font-weight": "bold",
3262
+ "text-valign": "bottom",
3263
+ "text-halign": "center",
3264
+ "text-wrap": "wrap",
3265
+ "text-max-width": "120px",
3266
+ // Increased from 100px for longer labels
3267
+ "text-margin-y": 5,
3268
+ // Subtle light gray label background
3269
+ "text-background-color": "#e5e7eb",
3270
+ // gray-200
3271
+ "text-background-opacity": 1,
3272
+ "text-background-padding": "3px",
3273
+ "text-background-shape": "roundrectangle",
3274
+ "border-width": 1,
3275
+ "border-color": "#ffffff",
3276
+ "border-opacity": 0.8
3277
+ }
3278
+ },
3279
+ // Override background color only when fill data is present
3280
+ {
3281
+ selector: "node[fill]",
3282
+ style: {
3283
+ "background-color": "data(fill)"
3284
+ }
3285
+ },
3286
+ {
3287
+ selector: "edge",
3288
+ style: {
3289
+ width: "data(size)",
3290
+ // Read width from element data
3291
+ "line-color": "data(color)",
3292
+ // Read color from element data
3293
+ "target-arrow-color": "data(color)",
3294
+ "target-arrow-shape": "triangle",
3295
+ "curve-style": "bezier",
3296
+ "control-point-step-size": 40,
3297
+ "control-point-weight": 0.5,
3298
+ opacity: 0.8,
3299
+ "arrow-scale": 0.8,
3300
+ "mid-target-arrow-shape": "none",
3301
+ "source-endpoint": "outside-to-node-or-label",
3302
+ "target-endpoint": "outside-to-node-or-label"
3303
+ }
3304
+ },
3305
+ // Dashed edges (when dashed property is true)
3306
+ {
3307
+ selector: "edge[dashed]",
3308
+ style: {
3309
+ "line-style": "dashed"
3310
+ }
3311
+ },
3312
+ // Show edge labels by default with good styling
3313
+ {
3314
+ selector: "edge[label]",
3315
+ style: {
3316
+ label: "data(label)",
3317
+ "font-size": "10px",
3318
+ // Increased from 8px for better readability
3319
+ "font-weight": "500",
3320
+ // Semi-bold for better visibility
3321
+ "text-background-color": "#ffffff",
3322
+ "text-background-opacity": 0.9,
3323
+ // Increased from 0.8 for better contrast
3324
+ "text-background-padding": "3px",
3325
+ // Increased from 2px
3326
+ "text-background-shape": "roundrectangle",
3327
+ "text-rotation": "autorotate",
3328
+ "text-margin-y": -8,
3329
+ // Adjusted for larger font
3330
+ color: "#1a1a1a"
3331
+ // Darker for better contrast
3332
+ }
3333
+ },
3334
+ {
3335
+ selector: "edge:selected",
3336
+ style: {
3337
+ "line-color": "#ff1a1a",
3338
+ "target-arrow-color": "#ff1a1a",
3339
+ width: 8,
3340
+ // even thicker
3341
+ "z-index": 999,
3342
+ "font-size": "11px",
3343
+ "font-weight": "bold",
3344
+ "text-background-opacity": 1,
3345
+ "text-background-color": "#ffffff",
3346
+ "text-background-padding": "3px",
3347
+ "arrow-scale": 1.35,
3348
+ opacity: 1,
3349
+ "overlay-color": "#ff1a1a",
3350
+ "overlay-opacity": 0.08
3351
+ }
3352
+ },
3353
+ {
3354
+ selector: "node:selected",
3355
+ style: {
3356
+ "border-width": 10,
3357
+ // thicker circle outline
3358
+ "border-color": "#ff1a1a",
3359
+ "border-opacity": 1,
3360
+ "background-opacity": 1,
3361
+ "text-background-opacity": 1,
3362
+ "text-background-color": "#ffffff",
3363
+ "text-background-padding": "4px",
3364
+ "overlay-color": "#ff1a1a",
3365
+ "overlay-opacity": 0.18,
3366
+ width: 48,
3367
+ // slight size bump from 40
3368
+ height: 48
3369
+ }
3370
+ }
3371
+ ],
3372
+ layout: { name: "grid", fit: true, avoidOverlap: true, condense: true },
3373
+ // initial quick layout
3374
+ wheelSensitivity: 0.2
3375
+ });
3376
+ const cy = cyRef.current;
3377
+ if (cy) {
3378
+ setupCytoscapeEventHandlers({
3379
+ cy,
3380
+ edgesRef,
3381
+ selectedNodeIdsRef,
3382
+ selectedEdgeRef,
3383
+ setSelectedNodeIdsStore,
3384
+ setSelectedEdgeStore,
3385
+ clearSelectionStore,
3386
+ useGraphExploration,
3387
+ onCopyNode
3388
+ });
3389
+ }
3390
+ return () => {
3391
+ cyRef.current?.destroy();
3392
+ cyRef.current = null;
3393
+ };
3394
+ }, []);
3395
+ const elementIdsRef = useRef2("");
3396
+ const lastLayoutRef = useRef2("");
3397
+ const lastFitPaddingRef = useRef2(fitPadding);
3398
+ useEffect3(() => {
3399
+ const cy = cyRef.current;
3400
+ if (!cy || !isInitializedRef.current) return;
3401
+ const currentIds = nodes.map((n) => n.id_).sort().join(",") + "|" + edges.map((e) => e.id_).sort().join(",");
3402
+ const action = classifyUpdate({
3403
+ cy,
3404
+ currentIds,
3405
+ elementIdsRef,
3406
+ lastLayoutRef,
3407
+ lastFitPaddingRef,
3408
+ currentLayout,
3409
+ fitPadding,
3410
+ nodeCount: nodes.length,
3411
+ edgeCount: edges.length
3412
+ });
3413
+ if (action === "refit") {
3414
+ lastFitPaddingRef.current = fitPadding;
3415
+ cy.fit(void 0, fitPadding);
3416
+ return;
3417
+ }
3418
+ if (action === "skip") {
3419
+ return;
3420
+ }
3421
+ elementIdsRef.current = currentIds;
3422
+ lastLayoutRef.current = currentLayout;
3423
+ lastFitPaddingRef.current = fitPadding;
3424
+ const { nodes: nodeElements, edges: edgeElements } = cytoData.elements;
3425
+ cy.batch(() => {
3426
+ cy.elements().remove();
3427
+ cy.add([...nodeElements, ...edgeElements]);
3428
+ });
3429
+ if (selectedNodeIdsRef.current.length > 0 || selectedEdgeRef.current) {
3430
+ cy.batch(() => {
3431
+ cy.elements().unselect();
3432
+ selectedNodeIdsRef.current.forEach((id) => cy.$id(id).select());
3433
+ if (selectedEdgeRef.current) {
3434
+ const edge = cy.$id(String(selectedEdgeRef.current.id_));
3435
+ if (edge) {
3436
+ edge.select();
3437
+ cy.$id(edge.data("source")).select();
3438
+ cy.$id(edge.data("target")).select();
3439
+ }
3440
+ }
3441
+ });
3442
+ }
3443
+ const chosen = ALLOWED_LAYOUTS.includes(
3444
+ currentLayout
3445
+ ) ? currentLayout : "auto";
3446
+ const nodesForLayout = nodes.map((n) => ({
3447
+ id: String(n.id_),
3448
+ data: { label_: n.label_ }
3449
+ }));
3450
+ applyLayout({
3451
+ cy,
3452
+ chosen,
3453
+ nodeCount: nodes.length,
3454
+ nodesForLayout,
3455
+ fitPadding
3456
+ });
3457
+ const pendingFocus = pendingFocusActionRef.current;
3458
+ if (pendingFocus) {
3459
+ processPendingFocus({
3460
+ cy,
3461
+ pendingFocus,
3462
+ edgesRef,
3463
+ setSelectedNodeIdsStore,
3464
+ setSelectedEdgeStore,
3465
+ selectedNodeIdsRef,
3466
+ selectedEdgeRef,
3467
+ pendingFocusActionRef
3468
+ });
3469
+ }
3470
+ }, [nodes, edges, currentLayout, fitPadding]);
3471
+ useEffect3(() => {
3472
+ if (!cyRef.current) return;
3473
+ cyRef.current.elements().unselect();
3474
+ clearSelectionStore();
3475
+ }, [lastDataChangeTime, clearSelectionStore]);
3476
+ useEffect3(() => {
3477
+ const unsubscribe = GraphFocus_default.subscribe((state) => {
3478
+ handleFocusStateChange({
3479
+ focusAction: state.focusAction,
3480
+ cyRef,
3481
+ edgesRef,
3482
+ selectedNodeIdsRef,
3483
+ selectedEdgeRef,
3484
+ setSelectedNodeIdsStore,
3485
+ setSelectedEdgeStore,
3486
+ pendingFocusActionRef,
3487
+ baseNormalized
3488
+ });
3489
+ });
3490
+ return unsubscribe;
3491
+ }, [baseNormalized, setSelectedEdgeStore, setSelectedNodeIdsStore]);
3492
+ const updateLayout = (newLayout) => {
3493
+ setCurrentLayout(newLayout);
3494
+ };
3495
+ const fitToScreen = useCallback3(() => {
3496
+ if (cyRef.current) {
3497
+ cyRef.current.fit(void 0, fitPadding);
3498
+ }
3499
+ }, [fitPadding]);
3500
+ useEffect3(() => {
3501
+ fitToScreen();
3502
+ }, [nodes.length, fitToScreen]);
3503
+ useEffect3(() => {
3504
+ if (!isInitializedRef.current) return;
3505
+ const handleResize = () => cyRef.current?.resize();
3506
+ window.addEventListener("resize", handleResize);
3507
+ return () => window.removeEventListener("resize", handleResize);
3508
+ }, []);
3509
+ const { fullscreen, toggleFullscreen } = useFullscreen(fitToScreen, cyRef);
3510
+ const {
3511
+ outerClass,
3512
+ innerClass,
3513
+ showEmptyMessage,
3514
+ showFilteredMessage,
3515
+ showControls
3516
+ } = computeGraphViewState({
3517
+ hasNodes: nodes.length > 0,
3518
+ hasHiddenElements,
3519
+ isFocusMode,
3520
+ fullscreen
3521
+ });
3522
+ return /* @__PURE__ */ jsx8("div", { className: outerClass, children: /* @__PURE__ */ jsxs7("div", { className: innerClass, children: [
3523
+ /* @__PURE__ */ jsx8("div", { ref: containerRef, className: "absolute inset-0" }),
3524
+ showEmptyMessage && /* @__PURE__ */ jsx8("div", { className: "absolute inset-0 flex items-center justify-center text-xs text-gray-500 pointer-events-none select-none", children: "No graph data to visualize (run a query or check the filters)" }),
3525
+ showFilteredMessage && /* @__PURE__ */ jsx8("div", { className: "absolute inset-0 flex items-center justify-center text-xs text-gray-500 pointer-events-none select-none", children: 'All elements are hidden or filtered (click "Reset Focus" to restore)' }),
3526
+ showControls && /* @__PURE__ */ jsx8(
3527
+ CytoscapeControls_default,
3528
+ {
3529
+ updateLayout,
3530
+ fitToScreen,
3531
+ onResetView: () => {
3532
+ resetView();
3533
+ toast3.success(
3534
+ isFocusMode ? "Focus mode cleared" : "Hidden elements restored"
3535
+ );
3536
+ },
3537
+ hasHiddenElements,
3538
+ isFocusMode,
3539
+ extraToolbarContent,
3540
+ zoomIn: () => {
3541
+ if (cyRef.current) {
3542
+ const z = cyRef.current.zoom();
3543
+ cyRef.current.zoom({
3544
+ level: Math.min(z * 1.2, 5),
3545
+ renderedPosition: {
3546
+ x: cyRef.current.width() / 2,
3547
+ y: cyRef.current.height() / 2
3548
+ }
3549
+ });
3550
+ }
3551
+ },
3552
+ zoomOut: () => {
3553
+ if (cyRef.current) {
3554
+ const z = cyRef.current.zoom();
3555
+ cyRef.current.zoom({
3556
+ level: Math.max(z / 1.2, 0.1),
3557
+ renderedPosition: {
3558
+ x: cyRef.current.width() / 2,
3559
+ y: cyRef.current.height() / 2
3560
+ }
3561
+ });
3562
+ }
3563
+ },
3564
+ currentLayout,
3565
+ fullscreen,
3566
+ toggleFullscreen
3567
+ }
3568
+ )
3569
+ ] }) });
3570
+ }
3571
+ var CytoscapeGraph_default = CytoscapeGraph;
3572
+
3573
+ // src/components/GraphVisualizer.tsx
3574
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3575
+ function GraphVisualizer({
3576
+ onCopyNode,
3577
+ extraToolbarContent
3578
+ }) {
3579
+ const { renderer } = useGraphRenderer();
3580
+ return /* @__PURE__ */ jsx9("div", { className: "relative w-full h-full flex flex-col overflow-hidden", children: /* @__PURE__ */ jsxs8("div", { className: "flex-1 h-full", children: [
3581
+ renderer === "reagraph" && /* @__PURE__ */ jsx9(
3582
+ ReaGraph_default,
3583
+ {
3584
+ onCopyNode,
3585
+ extraToolbarContent
3586
+ },
3587
+ "reagraph"
3588
+ ),
3589
+ renderer === "cytoscape" && /* @__PURE__ */ jsx9(
3590
+ CytoscapeGraph_default,
3591
+ {
3592
+ onCopyNode,
3593
+ extraToolbarContent
3594
+ },
3595
+ "cytoscape"
3596
+ )
3597
+ ] }) });
3598
+ }
3599
+ var GraphVisualizer_default = GraphVisualizer;
3600
+
3601
+ // src/components/ActionPanel/ActionPanel.tsx
3602
+ import { Card as Card5 } from "@petrarca/sonnet-ui";
3603
+
3604
+ // src/components/ActionPanel/NodeInfoCard.tsx
3605
+ import { useState as useState5 } from "react";
3606
+ import {
3607
+ Card as Card3,
3608
+ Collapsible,
3609
+ CollapsibleContent,
3610
+ CollapsibleTrigger
3611
+ } from "@petrarca/sonnet-ui";
3612
+ import { copyTextToClipboard } from "@petrarca/sonnet-core";
3613
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3614
+ function NodeInfoCard({ node }) {
3615
+ const [showProperties, setShowProperties] = useState5(false);
3616
+ return /* @__PURE__ */ jsxs9(
3617
+ Card3,
3618
+ {
3619
+ className: "w-full rounded-lg overflow-hidden bg-gray-100 border p-4 shadow-xs",
3620
+ style: {
3621
+ background: "#f3f4f6",
3622
+ borderLeft: `4px solid ${node.visual.color}`
3623
+ },
3624
+ children: [
3625
+ /* @__PURE__ */ jsx10("p", { className: "leading-tight font-semibold mb-0", children: node.label_ }),
3626
+ /* @__PURE__ */ jsx10("p", { className: "leading-tight mb-1", children: node.visual.displayName }),
3627
+ /* @__PURE__ */ jsx10(
3628
+ "p",
3629
+ {
3630
+ className: "text-xs text-muted-foreground cursor-pointer select-all mb-2",
3631
+ onClick: () => copyTextToClipboard(String(node.id_)),
3632
+ title: "Click to copy id",
3633
+ children: node.id_
3634
+ }
3635
+ ),
3636
+ /* @__PURE__ */ jsxs9(Collapsible, { open: showProperties, onOpenChange: setShowProperties, children: [
3637
+ /* @__PURE__ */ jsx10(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsx10("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
3638
+ /* @__PURE__ */ jsx10(CollapsibleContent, { children: /* @__PURE__ */ jsxs9("ul", { className: "list-none mt-2", children: [
3639
+ /* @__PURE__ */ jsx10("p", { className: "text-sm font-semibold mb-1 tracking-wide", children: "Properties" }),
3640
+ Object.entries(node.properties_ || {}).map(([key, value]) => /* @__PURE__ */ jsxs9("li", { className: "mb-2 leading-tight", children: [
3641
+ /* @__PURE__ */ jsx10("p", { className: "text-xs text-slate-600 uppercase tracking-wide", children: key }),
3642
+ /* @__PURE__ */ jsx10(
3643
+ "p",
3644
+ {
3645
+ className: "text-sm truncate max-w-[190px] cursor-pointer",
3646
+ onClick: () => copyTextToClipboard(String(value)),
3647
+ title: "Click to copy value",
3648
+ children: String(value)
3649
+ }
3650
+ )
3651
+ ] }, key))
3652
+ ] }) })
3653
+ ] })
3654
+ ]
3655
+ }
3656
+ );
3657
+ }
3658
+ var NodeInfoCard_default = NodeInfoCard;
3659
+
3660
+ // src/components/ActionPanel/EdgeInfoCard.tsx
3661
+ import { useState as useState6 } from "react";
3662
+ import {
3663
+ Card as Card4,
3664
+ Collapsible as Collapsible2,
3665
+ CollapsibleContent as CollapsibleContent2,
3666
+ CollapsibleTrigger as CollapsibleTrigger2
3667
+ } from "@petrarca/sonnet-ui";
3668
+ import { ArrowDown } from "lucide-react";
3669
+ import { copyTextToClipboard as copyTextToClipboard2 } from "@petrarca/sonnet-core";
3670
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
3671
+ function EdgeInfoCard({ edge }) {
3672
+ const [showProperties, setShowProperties] = useState6(false);
3673
+ return /* @__PURE__ */ jsxs10(
3674
+ Card4,
3675
+ {
3676
+ className: "w-full rounded-lg overflow-hidden bg-gray-100 border p-4 shadow-xs",
3677
+ style: {
3678
+ background: "#f3f4f6",
3679
+ borderLeft: `4px solid ${edge.visual.color}`
3680
+ },
3681
+ children: [
3682
+ /* @__PURE__ */ jsxs10("p", { className: "leading-tight mb-1", children: [
3683
+ /* @__PURE__ */ jsx11(ArrowDown, { className: "inline-block mr-1 opacity-70 align-middle" }),
3684
+ /* @__PURE__ */ jsx11("strong", { className: "font-semibold align-middle", children: edge.label_ })
3685
+ ] }),
3686
+ edge.id_ && /* @__PURE__ */ jsx11(
3687
+ "p",
3688
+ {
3689
+ className: "text-xs text-muted-foreground select-all cursor-pointer mt-1 mb-2",
3690
+ title: "Click to copy edge id",
3691
+ onClick: () => copyTextToClipboard2(String(edge.id_)),
3692
+ children: edge.id_
3693
+ }
3694
+ ),
3695
+ edge.properties_ && Object.keys(edge.properties_).length > 0 && /* @__PURE__ */ jsxs10(Collapsible2, { open: showProperties, onOpenChange: setShowProperties, children: [
3696
+ /* @__PURE__ */ jsx11(CollapsibleTrigger2, { asChild: true, children: /* @__PURE__ */ jsx11("button", { className: "text-xs cursor-pointer", children: showProperties ? "Hide" : "Show more" }) }),
3697
+ /* @__PURE__ */ jsx11(CollapsibleContent2, { children: /* @__PURE__ */ jsxs10("ul", { className: "list-none mt-2", children: [
3698
+ /* @__PURE__ */ jsx11("p", { className: "text-sm font-semibold mb-1 tracking-wide", children: "Properties" }),
3699
+ Object.entries(edge.properties_ || {}).map(([key, value]) => /* @__PURE__ */ jsxs10("li", { className: "mb-2 leading-tight", children: [
3700
+ /* @__PURE__ */ jsx11("p", { className: "text-xs text-slate-600 uppercase tracking-wide", children: key }),
3701
+ /* @__PURE__ */ jsx11(
3702
+ "p",
3703
+ {
3704
+ className: "text-sm truncate max-w-[190px] cursor-pointer",
3705
+ onClick: () => copyTextToClipboard2(String(value)),
3706
+ title: "Click to copy value",
3707
+ children: String(value)
3708
+ }
3709
+ )
3710
+ ] }, key))
3711
+ ] }) })
3712
+ ] })
3713
+ ]
3714
+ }
3715
+ );
3716
+ }
3717
+ var EdgeInfoCard_default = EdgeInfoCard;
3718
+
3719
+ // src/components/ActionPanel/Overview.tsx
3720
+ import { Badge } from "@petrarca/sonnet-ui";
3721
+ import { formatNumber, devLog as devLog11 } from "@petrarca/sonnet-core";
3722
+ import { useMemo as useMemo4 } from "react";
3723
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
3724
+ function computeEdgeToNodeLabels(nodesById, edgesById) {
3725
+ const mapping = {};
3726
+ for (const edge of Object.values(edgesById)) {
3727
+ const edgeLabel = edge.label_;
3728
+ const startNode = nodesById[String(edge.start_id_)];
3729
+ const endNode = nodesById[String(edge.end_id_)];
3730
+ if (!mapping[edgeLabel]) {
3731
+ mapping[edgeLabel] = /* @__PURE__ */ new Set();
3732
+ }
3733
+ if (startNode) mapping[edgeLabel].add(startNode.label_);
3734
+ if (endNode) mapping[edgeLabel].add(endNode.label_);
3735
+ }
3736
+ const result = {};
3737
+ for (const [edgeLabel, nodeLabels] of Object.entries(mapping)) {
3738
+ result[edgeLabel] = Array.from(nodeLabels);
3739
+ }
3740
+ return result;
3741
+ }
3742
+ function resolveBadgeColor(isAllDisabled, filters, label, colorPalette, activeColor) {
3743
+ if (isAllDisabled) return colorPalette["faded"];
3744
+ if (filters.length === 0 || filters.includes(label))
3745
+ return colorPalette[activeColor];
3746
+ return colorPalette["faded"];
3747
+ }
3748
+ function AllToggleBadge({
3749
+ allDisabled,
3750
+ colorPalette,
3751
+ count,
3752
+ onToggle,
3753
+ badgeKey
3754
+ }) {
3755
+ const bgColor = allDisabled ? colorPalette["faded"] : colorPalette["neutral"];
3756
+ return /* @__PURE__ */ jsxs11(
3757
+ Badge,
3758
+ {
3759
+ className: "cursor-pointer text-sm px-3 py-1",
3760
+ style: {
3761
+ textTransform: "none",
3762
+ backgroundColor: bgColor,
3763
+ borderRadius: "10px"
3764
+ },
3765
+ onClick: onToggle,
3766
+ children: [
3767
+ "* (",
3768
+ formatNumber(count),
3769
+ ")"
3770
+ ]
3771
+ },
3772
+ badgeKey
3773
+ );
3774
+ }
3775
+ function NodeLabelBadges({
3776
+ labels,
3777
+ labelCounts,
3778
+ colorPalette,
3779
+ allLabelsDisabled,
3780
+ nodeLabelFilters,
3781
+ toggleNodeLabelFilter
3782
+ }) {
3783
+ return labels.map((label) => {
3784
+ const bgColor = resolveBadgeColor(
3785
+ allLabelsDisabled,
3786
+ nodeLabelFilters,
3787
+ label,
3788
+ colorPalette,
3789
+ label
3790
+ );
3791
+ return /* @__PURE__ */ jsxs11(
3792
+ Badge,
3793
+ {
3794
+ className: "cursor-pointer text-sm px-3 py-1",
3795
+ style: {
3796
+ textTransform: "none",
3797
+ backgroundColor: bgColor,
3798
+ borderRadius: "10px"
3799
+ },
3800
+ onClick: () => toggleNodeLabelFilter(label, labels),
3801
+ children: [
3802
+ label,
3803
+ " (",
3804
+ formatNumber(labelCounts[label]),
3805
+ ")"
3806
+ ]
3807
+ },
3808
+ label
3809
+ );
3810
+ });
3811
+ }
3812
+ function EdgeLabelBadges({
3813
+ labels,
3814
+ edgeLabelCounts,
3815
+ colorPalette,
3816
+ allEdgeLabelsDisabled,
3817
+ edgeLabelFilters,
3818
+ toggleEdgeLabelFilter,
3819
+ edgeToNodeLabels
3820
+ }) {
3821
+ return labels.map((label) => {
3822
+ const bgColor = resolveBadgeColor(
3823
+ allEdgeLabelsDisabled,
3824
+ edgeLabelFilters,
3825
+ label,
3826
+ colorPalette,
3827
+ "neutral"
3828
+ );
3829
+ return /* @__PURE__ */ jsxs11(
3830
+ Badge,
3831
+ {
3832
+ className: "cursor-pointer text-sm px-3 py-1",
3833
+ style: {
3834
+ textTransform: "none",
3835
+ backgroundColor: bgColor,
3836
+ borderRadius: "10px"
3837
+ },
3838
+ onClick: () => toggleEdgeLabelFilter(label, labels, edgeToNodeLabels),
3839
+ children: [
3840
+ label,
3841
+ " (",
3842
+ formatNumber(edgeLabelCounts[label]),
3843
+ ")"
3844
+ ]
3845
+ },
3846
+ label
3847
+ );
3848
+ });
3849
+ }
3850
+ function GraphOverview({
3851
+ stats,
3852
+ colorPalette,
3853
+ labelCounts,
3854
+ edgeLabelCounts,
3855
+ edgeToNodeLabels
3856
+ }) {
3857
+ const useGraphFilter2 = useWorkspaceGraphFilter();
3858
+ const nodeLabelFilters = useGraphFilter2((state) => state.nodeLabelFilters);
3859
+ const allLabelsDisabled = useGraphFilter2((state) => state.allLabelsDisabled);
3860
+ const edgeLabelFilters = useGraphFilter2((state) => state.edgeLabelFilters);
3861
+ const allEdgeLabelsDisabled = useGraphFilter2(
3862
+ (state) => state.allEdgeLabelsDisabled
3863
+ );
3864
+ const toggleNodeLabelFilter = useGraphFilter2(
3865
+ (state) => state.toggleNodeLabelFilter
3866
+ );
3867
+ const clearNodeLabelFilters = useGraphFilter2(
3868
+ (state) => state.clearNodeLabelFilters
3869
+ );
3870
+ const toggleAllLabelsDisabled = useGraphFilter2(
3871
+ (state) => state.toggleAllLabelsDisabled
3872
+ );
3873
+ const toggleEdgeLabelFilter = useGraphFilter2(
3874
+ (state) => state.toggleEdgeLabelFilter
3875
+ );
3876
+ const clearEdgeLabelFilters = useGraphFilter2(
3877
+ (state) => state.clearEdgeLabelFilters
3878
+ );
3879
+ const toggleAllEdgeLabelsDisabled = useGraphFilter2(
3880
+ (state) => state.toggleAllEdgeLabelsDisabled
3881
+ );
3882
+ const allLabels = Object.keys(labelCounts).sort();
3883
+ devLog11("[Overview] Rendering labels", {
3884
+ labels: allLabels,
3885
+ colorKeys: Object.keys(colorPalette || {}),
3886
+ labelCounts,
3887
+ stats
3888
+ });
3889
+ const allEdgeLabels = Object.keys(edgeLabelCounts || {}).sort();
3890
+ const handleNodeToggle = () => {
3891
+ if (allLabelsDisabled) {
3892
+ toggleAllLabelsDisabled();
3893
+ clearNodeLabelFilters();
3894
+ } else {
3895
+ toggleAllLabelsDisabled();
3896
+ }
3897
+ };
3898
+ const handleEdgeToggle = () => {
3899
+ if (allEdgeLabelsDisabled) {
3900
+ toggleAllEdgeLabelsDisabled();
3901
+ clearEdgeLabelFilters();
3902
+ } else {
3903
+ toggleAllEdgeLabelsDisabled();
3904
+ }
3905
+ };
3906
+ return /* @__PURE__ */ jsxs11(Fragment3, { children: [
3907
+ /* @__PURE__ */ jsx12("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
3908
+ allLabels.length > 0 && /* @__PURE__ */ jsxs11("div", { children: [
3909
+ /* @__PURE__ */ jsx12("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Node Labels" }),
3910
+ /* @__PURE__ */ jsxs11("div", { className: "flex flex-col items-start gap-2", children: [
3911
+ /* @__PURE__ */ jsx12(
3912
+ AllToggleBadge,
3913
+ {
3914
+ allDisabled: allLabelsDisabled,
3915
+ colorPalette,
3916
+ count: stats.nodes,
3917
+ onToggle: handleNodeToggle,
3918
+ badgeKey: "all"
3919
+ }
3920
+ ),
3921
+ /* @__PURE__ */ jsx12(
3922
+ NodeLabelBadges,
3923
+ {
3924
+ labels: allLabels,
3925
+ labelCounts,
3926
+ colorPalette,
3927
+ allLabelsDisabled,
3928
+ nodeLabelFilters,
3929
+ toggleNodeLabelFilter
3930
+ }
3931
+ )
3932
+ ] })
3933
+ ] }),
3934
+ allEdgeLabels.length > 0 && /* @__PURE__ */ jsxs11("div", { children: [
3935
+ /* @__PURE__ */ jsx12("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Edge Types" }),
3936
+ /* @__PURE__ */ jsxs11("div", { className: "flex flex-col items-start gap-2", children: [
3937
+ /* @__PURE__ */ jsx12(
3938
+ AllToggleBadge,
3939
+ {
3940
+ allDisabled: allEdgeLabelsDisabled,
3941
+ colorPalette,
3942
+ count: stats.edges,
3943
+ onToggle: handleEdgeToggle,
3944
+ badgeKey: "edge-all"
3945
+ }
3946
+ ),
3947
+ /* @__PURE__ */ jsx12(
3948
+ EdgeLabelBadges,
3949
+ {
3950
+ labels: allEdgeLabels,
3951
+ edgeLabelCounts,
3952
+ colorPalette,
3953
+ allEdgeLabelsDisabled,
3954
+ edgeLabelFilters,
3955
+ toggleEdgeLabelFilter,
3956
+ edgeToNodeLabels
3957
+ }
3958
+ )
3959
+ ] })
3960
+ ] })
3961
+ ] });
3962
+ }
3963
+ function Overview() {
3964
+ const { stats, colorPalette, labelCounts, edgeLabelCounts, baseNormalized } = useLiveGraphData();
3965
+ const edgeToNodeLabels = useMemo4(() => {
3966
+ if (!baseNormalized?.nodesById || !baseNormalized?.edgesById) return {};
3967
+ return computeEdgeToNodeLabels(
3968
+ baseNormalized.nodesById,
3969
+ baseNormalized.edgesById
3970
+ );
3971
+ }, [baseNormalized]);
3972
+ if (stats && stats.values != null && stats.values > 0) {
3973
+ return /* @__PURE__ */ jsxs11(Fragment3, { children: [
3974
+ /* @__PURE__ */ jsx12("p", { className: "mt-2 text-sm font-semibold", children: "Overview" }),
3975
+ /* @__PURE__ */ jsxs11("p", { className: "text-sm text-slate-700", children: [
3976
+ formatNumber(stats.values),
3977
+ " Values"
3978
+ ] })
3979
+ ] });
3980
+ }
3981
+ if (stats && stats.nodes > 0 && colorPalette) {
3982
+ return /* @__PURE__ */ jsx12(
3983
+ GraphOverview,
3984
+ {
3985
+ stats,
3986
+ colorPalette,
3987
+ labelCounts,
3988
+ edgeLabelCounts,
3989
+ edgeToNodeLabels
3990
+ }
3991
+ );
3992
+ }
3993
+ return null;
3994
+ }
3995
+ var Overview_default = Overview;
3996
+
3997
+ // src/components/ActionPanel/ActionPanel.tsx
3998
+ import { useMemo as useMemo5 } from "react";
3999
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
4000
+ function ActionPanel({ actions }) {
4001
+ const useSelection3 = useWorkspaceSelection();
4002
+ const selectedNodeIds = useSelection3(
4003
+ (s) => s.selectedNodeIds
4004
+ );
4005
+ const selectedEdge = useSelection3((s) => s.selectedEdge);
4006
+ const live = useLiveGraphData();
4007
+ const selectedNodes = useMemo5(
4008
+ () => selectedNodeIds.map((id) => live.normalized.nodesById[id]).filter(Boolean),
4009
+ [selectedNodeIds, live.normalized]
4010
+ );
4011
+ const firstNode = selectedNodes[0];
4012
+ const secondNode = selectedNodes?.[1];
4013
+ const hasEdge = !!selectedEdge;
4014
+ const showOverview = !firstNode && !hasEdge;
4015
+ return /* @__PURE__ */ jsxs12(
4016
+ Card5,
4017
+ {
4018
+ className: "h-full min-w-64 flex flex-col bg-gray-50 shadow-xs p-4",
4019
+ style: { maxHeight: "100%", overflow: "hidden" },
4020
+ children: [
4021
+ actions && /* @__PURE__ */ jsx13("div", { className: "flex-shrink-0 mb-2", children: actions }),
4022
+ /* @__PURE__ */ jsx13("div", { className: "flex-1 overflow-auto pr-1", children: /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-4", children: [
4023
+ firstNode && /* @__PURE__ */ jsx13(NodeInfoCard_default, { node: firstNode }),
4024
+ hasEdge && selectedEdge && /* @__PURE__ */ jsx13(EdgeInfoCard_default, { edge: selectedEdge }),
4025
+ secondNode && /* @__PURE__ */ jsx13(NodeInfoCard_default, { node: secondNode }),
4026
+ showOverview && /* @__PURE__ */ jsx13(Overview_default, {})
4027
+ ] }) })
4028
+ ]
4029
+ }
4030
+ );
4031
+ }
4032
+ var ActionPanel_default = ActionPanel;
4033
+ export {
4034
+ ActionPanel_default as ActionPanel,
4035
+ EMPTY_NORMALIZED,
4036
+ EdgeInfoCard_default as EdgeInfoCard,
4037
+ GraphRendererProvider,
4038
+ GraphStoresContext,
4039
+ GraphVisualizer_default as GraphVisualizer,
4040
+ NodeInfoCard_default as NodeInfoCard,
4041
+ Overview_default as Overview,
4042
+ buildColorPalette,
4043
+ buildColorPaletteFromNodes,
4044
+ createActionPanelStore,
4045
+ createGraphDataStore,
4046
+ createGraphExplorationStore,
4047
+ createGraphFilterStore,
4048
+ createGraphFocusStore,
4049
+ createSelectionStore,
4050
+ enrichEdge,
4051
+ enrichEdges,
4052
+ enrichNode,
4053
+ enrichNodes,
4054
+ extractDisplayLabel,
4055
+ isRawGraphNode,
4056
+ isVisualEdge,
4057
+ isVisualNode,
4058
+ normalizeGraph,
4059
+ toCytoscapeEdge,
4060
+ toCytoscapeNode,
4061
+ toReagraphEdge,
4062
+ toReagraphNode,
4063
+ useActionPanelStore,
4064
+ useCytoscapeData,
4065
+ GraphFocus_default as useGraphFocus,
4066
+ useGraphRenderer,
4067
+ useLiveGraphData,
4068
+ useReagraphData,
4069
+ useWorkspaceGraphData,
4070
+ useWorkspaceGraphExploration,
4071
+ useWorkspaceGraphFilter,
4072
+ useWorkspaceSelection
4073
+ };
4074
+ //# sourceMappingURL=index.js.map