@railtownai/railtracks-visualizer 0.0.3 → 0.0.5
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/LICENSE +201 -201
- package/README.md +52 -23
- package/dist/cjs/index.js +55 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +55 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/{AgenticFlowVisualizer.d.ts → types/src/AgenticFlowVisualizer.d.ts} +2 -2
- package/dist/types/src/AgenticFlowVisualizer.d.ts.map +1 -0
- package/dist/types/src/App.d.ts.map +1 -0
- package/dist/types/src/Visualizer.d.ts +9 -0
- package/dist/types/src/Visualizer.d.ts.map +1 -0
- package/dist/types/src/components/Edge.d.ts.map +1 -0
- package/dist/types/src/components/FileSelector.d.ts.map +1 -0
- package/dist/types/src/components/Node.d.ts.map +1 -0
- package/dist/types/src/components/Timeline.d.ts.map +1 -0
- package/dist/types/src/components/VerticalTimeline.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useApi.d.ts.map +1 -0
- package/dist/types/src/hooks/useFlowData.d.ts.map +1 -0
- package/dist/{index.d.ts → types/src/index.d.ts} +1 -2
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/tests/setup.d.ts +2 -0
- package/dist/types/tests/setup.d.ts.map +1 -0
- package/package.json +93 -75
- package/dist/AgenticFlowVisualizer.d.ts.map +0 -1
- package/dist/AgenticFlowVisualizer.js +0 -710
- package/dist/App.d.ts.map +0 -1
- package/dist/App.js +0 -89
- package/dist/components/Edge.d.ts.map +0 -1
- package/dist/components/Edge.js +0 -74
- package/dist/components/FileSelector.d.ts.map +0 -1
- package/dist/components/FileSelector.js +0 -24
- package/dist/components/Node.d.ts.map +0 -1
- package/dist/components/Node.js +0 -100
- package/dist/components/Timeline.d.ts.map +0 -1
- package/dist/components/Timeline.js +0 -118
- package/dist/components/VerticalTimeline.d.ts.map +0 -1
- package/dist/components/VerticalTimeline.js +0 -156
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/useApi.d.ts.map +0 -1
- package/dist/hooks/useApi.js +0 -95
- package/dist/hooks/useFlowData.d.ts.map +0 -1
- package/dist/hooks/useFlowData.js +0 -66
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -13
- /package/dist/{App.d.ts → types/src/App.d.ts} +0 -0
- /package/dist/{components → types/src/components}/Edge.d.ts +0 -0
- /package/dist/{components → types/src/components}/FileSelector.d.ts +0 -0
- /package/dist/{components → types/src/components}/Node.d.ts +0 -0
- /package/dist/{components → types/src/components}/Timeline.d.ts +0 -0
- /package/dist/{components → types/src/components}/VerticalTimeline.d.ts +0 -0
- /package/dist/{hooks → types/src/hooks}/index.d.ts +0 -0
- /package/dist/{hooks → types/src/hooks}/useApi.d.ts +0 -0
- /package/dist/{hooks → types/src/hooks}/useFlowData.d.ts +0 -0
|
@@ -1,710 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { useReactFlow } from "reactflow";
|
|
4
|
-
import ReactFlow, { Controls, Background, addEdge, useNodesState, useEdgesState } from "reactflow";
|
|
5
|
-
import "reactflow/dist/style.css";
|
|
6
|
-
import { PanelLeft, PanelRight } from "lucide-react";
|
|
7
|
-
import { Edge as RCEdge } from "./components/Edge";
|
|
8
|
-
import { Node as RCNode } from "./components/Node";
|
|
9
|
-
import { Timeline } from "./components/Timeline";
|
|
10
|
-
import { VerticalTimeline } from "./components/VerticalTimeline";
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// UTILITY FUNCTIONS
|
|
13
|
-
// ============================================================================
|
|
14
|
-
/**
|
|
15
|
-
* Calculates a clean tree layout: parents centered above children, siblings spaced evenly, no overlap.
|
|
16
|
-
*/
|
|
17
|
-
const calculateAutoLayout = (nodes, edges) => {
|
|
18
|
-
// Build maps for fast lookup
|
|
19
|
-
const nodeMap = new Map(nodes.map((n) => [n.identifier, n]));
|
|
20
|
-
const childrenMap = new Map();
|
|
21
|
-
const parentMap = new Map();
|
|
22
|
-
nodes.forEach((n) => childrenMap.set(n.identifier, []));
|
|
23
|
-
// Handle edges if they exist
|
|
24
|
-
if (edges.length > 0) {
|
|
25
|
-
edges.forEach((e) => {
|
|
26
|
-
if (e.source && e.target) {
|
|
27
|
-
childrenMap.get(e.source)?.push(e.target);
|
|
28
|
-
parentMap.set(e.target, e.source);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
// If no edges, try to infer relationships from parent field
|
|
34
|
-
nodes.forEach((node) => {
|
|
35
|
-
if (node.parent && node.parent.identifier !== node.identifier) {
|
|
36
|
-
childrenMap.get(node.parent.identifier)?.push(node.identifier);
|
|
37
|
-
parentMap.set(node.identifier, node.parent.identifier);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
// Find root nodes (no parent)
|
|
42
|
-
const roots = nodes.filter((n) => !parentMap.has(n.identifier));
|
|
43
|
-
// Assign levels (depths) - now vertical levels from top to bottom
|
|
44
|
-
const levelMap = new Map();
|
|
45
|
-
const assignLevels = (nodeId, level) => {
|
|
46
|
-
levelMap.set(nodeId, level);
|
|
47
|
-
for (const childId of childrenMap.get(nodeId) || []) {
|
|
48
|
-
assignLevels(childId, level + 1);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
roots.forEach((root) => assignLevels(root.identifier, 0));
|
|
52
|
-
// Group nodes by level
|
|
53
|
-
const levels = [];
|
|
54
|
-
nodes.forEach((n) => {
|
|
55
|
-
const lvl = levelMap.get(n.identifier) ?? 0;
|
|
56
|
-
if (!levels[lvl])
|
|
57
|
-
levels[lvl] = [];
|
|
58
|
-
levels[lvl].push(n.identifier);
|
|
59
|
-
});
|
|
60
|
-
// Calculate subtree widths for each node (for centering horizontally)
|
|
61
|
-
const subtreeWidth = new Map();
|
|
62
|
-
const nodeWidth = 200; // Approximate node width
|
|
63
|
-
const nodeSpacing = 100; // Horizontal spacing between nodes
|
|
64
|
-
const calcSubtreeWidth = (nodeId) => {
|
|
65
|
-
const children = childrenMap.get(nodeId) || [];
|
|
66
|
-
if (children.length === 0) {
|
|
67
|
-
subtreeWidth.set(nodeId, nodeWidth);
|
|
68
|
-
return nodeWidth;
|
|
69
|
-
}
|
|
70
|
-
let width = 0;
|
|
71
|
-
for (const childId of children) {
|
|
72
|
-
width += calcSubtreeWidth(childId);
|
|
73
|
-
}
|
|
74
|
-
width += (children.length - 1) * nodeSpacing;
|
|
75
|
-
subtreeWidth.set(nodeId, width);
|
|
76
|
-
return width;
|
|
77
|
-
};
|
|
78
|
-
roots.forEach((root) => calcSubtreeWidth(root.identifier));
|
|
79
|
-
// Assign positions recursively - now top to bottom
|
|
80
|
-
const positions = new Map();
|
|
81
|
-
const levelSpacing = 300; // Increased vertical spacing between levels
|
|
82
|
-
const verticalMargin = 100; // Add margin to the top
|
|
83
|
-
const assignPositions = (nodeId, xLeft, level) => {
|
|
84
|
-
const width = subtreeWidth.get(nodeId) || nodeWidth;
|
|
85
|
-
const y = level * levelSpacing + verticalMargin; // Add margin here
|
|
86
|
-
const children = childrenMap.get(nodeId) || [];
|
|
87
|
-
if (children.length === 0) {
|
|
88
|
-
// Leaf node: center in its width
|
|
89
|
-
positions.set(nodeId, { x: xLeft + width / 2 - nodeWidth / 2, y });
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
// Internal node: center above its children
|
|
93
|
-
let childX = xLeft;
|
|
94
|
-
for (const childId of children) {
|
|
95
|
-
const childWidth = subtreeWidth.get(childId) || nodeWidth;
|
|
96
|
-
assignPositions(childId, childX, level + 1);
|
|
97
|
-
childX += childWidth + nodeSpacing;
|
|
98
|
-
}
|
|
99
|
-
// Center parent above children
|
|
100
|
-
const firstChild = children[0];
|
|
101
|
-
const lastChild = children[children.length - 1];
|
|
102
|
-
const firstPos = positions.get(firstChild);
|
|
103
|
-
const lastPos = positions.get(lastChild);
|
|
104
|
-
const parentX = (firstPos.x + lastPos.x) / 2;
|
|
105
|
-
positions.set(nodeId, { x: parentX, y });
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
// Lay out each tree
|
|
109
|
-
let xCursor = 0;
|
|
110
|
-
for (const root of roots) {
|
|
111
|
-
assignPositions(root.identifier, xCursor, 0);
|
|
112
|
-
xCursor += (subtreeWidth.get(root.identifier) || nodeWidth) + nodeSpacing * 2;
|
|
113
|
-
}
|
|
114
|
-
return positions;
|
|
115
|
-
};
|
|
116
|
-
/**
|
|
117
|
-
* Extracts LLM details from node data for display
|
|
118
|
-
*/
|
|
119
|
-
const extractLLMDetails = (node) => {
|
|
120
|
-
let description = node.node_type;
|
|
121
|
-
let modelInfo = "";
|
|
122
|
-
if (node.details?.internals?.llm_details) {
|
|
123
|
-
const llmDetails = node.details.internals.llm_details;
|
|
124
|
-
if (llmDetails.length > 0) {
|
|
125
|
-
const lastLLM = llmDetails[llmDetails.length - 1];
|
|
126
|
-
modelInfo = `${lastLLM.model_name} (${lastLLM.model_provider})`;
|
|
127
|
-
description = `${node.node_type}\n${modelInfo}`;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return { description, modelInfo };
|
|
131
|
-
};
|
|
132
|
-
/**
|
|
133
|
-
* Truncates text to specified length with ellipsis
|
|
134
|
-
*/
|
|
135
|
-
const truncateText = (text, maxLength) => {
|
|
136
|
-
if (text.length <= maxLength)
|
|
137
|
-
return text;
|
|
138
|
-
return text.substring(0, maxLength) + "...";
|
|
139
|
-
};
|
|
140
|
-
const nodeTypes = {
|
|
141
|
-
agent: RCNode
|
|
142
|
-
};
|
|
143
|
-
const edgeTypes = {
|
|
144
|
-
default: RCEdge
|
|
145
|
-
};
|
|
146
|
-
const AgenticFlowVisualizer = ({ flowData: propFlowData, width = "100%", height = "1000px", className = "" }) => {
|
|
147
|
-
// Use prop data directly, no hooks needed
|
|
148
|
-
const flowData = propFlowData;
|
|
149
|
-
// Show no data state if no flowData is available
|
|
150
|
-
if (!flowData) {
|
|
151
|
-
return (_jsx("div", { style: {
|
|
152
|
-
width: typeof width === "number" ? `${width}px` : width,
|
|
153
|
-
height: typeof height === "number" ? `${height}px` : height,
|
|
154
|
-
border: "1px solid #e5e7eb",
|
|
155
|
-
borderRadius: "8px",
|
|
156
|
-
background: "#f9fafb",
|
|
157
|
-
position: "relative",
|
|
158
|
-
minWidth: "800px",
|
|
159
|
-
minHeight: "600px",
|
|
160
|
-
display: "flex",
|
|
161
|
-
alignItems: "center",
|
|
162
|
-
justifyContent: "center"
|
|
163
|
-
}, className: className, children: _jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: "16px", color: "#6b7280", marginBottom: "8px" }, children: "No flow data available" }), _jsx("div", { style: { fontSize: "14px", color: "#9ca3af" }, children: "Please provide flowData prop" })] }) }));
|
|
164
|
-
}
|
|
165
|
-
const containerRef = useRef(null);
|
|
166
|
-
const svgRef = useRef(null);
|
|
167
|
-
const [containerDimensions, setContainerDimensions] = useState({
|
|
168
|
-
width: typeof width === "number" ? width : 800,
|
|
169
|
-
height: typeof height === "number" ? height : 600
|
|
170
|
-
});
|
|
171
|
-
// Timeline state
|
|
172
|
-
const [currentStep, setCurrentStep] = useState(0);
|
|
173
|
-
const [isPlaying, setIsPlaying] = useState(false);
|
|
174
|
-
const playIntervalRef = useRef(null);
|
|
175
|
-
// Timeline visibility state
|
|
176
|
-
const [showTimelines, setShowTimelines] = useState(false);
|
|
177
|
-
// Drawer state
|
|
178
|
-
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
179
|
-
const [selectedData, setSelectedData] = useState(null);
|
|
180
|
-
// Get max step from stamps or steps
|
|
181
|
-
const maxStep = useMemo(() => {
|
|
182
|
-
const stamps = flowData.stamps || flowData.steps || [];
|
|
183
|
-
return stamps.length > 0 ? Math.max(...stamps.map((s) => s.step)) : 0;
|
|
184
|
-
}, [flowData.stamps, flowData.steps]);
|
|
185
|
-
// Auto-play functionality
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
if (isPlaying) {
|
|
188
|
-
playIntervalRef.current = setInterval(() => {
|
|
189
|
-
setCurrentStep((prev) => {
|
|
190
|
-
if (prev >= maxStep) {
|
|
191
|
-
setIsPlaying(false);
|
|
192
|
-
return prev;
|
|
193
|
-
}
|
|
194
|
-
return prev + 1;
|
|
195
|
-
});
|
|
196
|
-
}, 250); // 1 second per step
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
if (playIntervalRef.current) {
|
|
200
|
-
clearInterval(playIntervalRef.current);
|
|
201
|
-
playIntervalRef.current = null;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return () => {
|
|
205
|
-
if (playIntervalRef.current) {
|
|
206
|
-
clearInterval(playIntervalRef.current);
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
}, [isPlaying, maxStep]);
|
|
210
|
-
// Initialize current step to last step and pan to hub node
|
|
211
|
-
useEffect(() => {
|
|
212
|
-
const stamps = flowData.stamps || flowData.steps || [];
|
|
213
|
-
if (stamps.length > 0) {
|
|
214
|
-
setCurrentStep(Math.max(...stamps.map((s) => s.step)));
|
|
215
|
-
}
|
|
216
|
-
}, [flowData.stamps, flowData.steps]);
|
|
217
|
-
// Update dimensions when width/height props change
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
const updateDimensions = () => {
|
|
220
|
-
if (containerRef.current) {
|
|
221
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
222
|
-
setContainerDimensions({
|
|
223
|
-
width: rect.width || (typeof width === "number" ? width : 800),
|
|
224
|
-
height: rect.height || (typeof height === "number" ? height : 600)
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
updateDimensions();
|
|
229
|
-
// Use ResizeObserver if available, otherwise fallback to window resize
|
|
230
|
-
if (window.ResizeObserver && containerRef.current) {
|
|
231
|
-
const resizeObserver = new ResizeObserver(updateDimensions);
|
|
232
|
-
resizeObserver.observe(containerRef.current);
|
|
233
|
-
return () => resizeObserver.disconnect();
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
window.addEventListener("resize", updateDimensions);
|
|
237
|
-
return () => window.removeEventListener("resize", updateDimensions);
|
|
238
|
-
}
|
|
239
|
-
}, [width, height]);
|
|
240
|
-
// Calculate auto-layout positions
|
|
241
|
-
const positions = useMemo(() => {
|
|
242
|
-
return calculateAutoLayout(flowData.nodes, flowData.edges || []);
|
|
243
|
-
}, [flowData.nodes, flowData.edges]);
|
|
244
|
-
// Get nodes and edges for current step
|
|
245
|
-
const getNodesForStep = useCallback((step) => {
|
|
246
|
-
return flowData.nodes.filter((node) => node.stamp.step <= step);
|
|
247
|
-
}, [flowData.nodes]);
|
|
248
|
-
const getEdgesForStep = useCallback((step) => {
|
|
249
|
-
return (flowData.edges || []).filter((edge) => edge.stamp.step <= step);
|
|
250
|
-
}, [flowData.edges]);
|
|
251
|
-
// Handle node inspection
|
|
252
|
-
const handleNodeInspect = useCallback((nodeData) => {
|
|
253
|
-
setSelectedData({ type: "node", data: nodeData });
|
|
254
|
-
setIsDrawerOpen(true);
|
|
255
|
-
}, []);
|
|
256
|
-
// Handle edge inspection
|
|
257
|
-
const handleEdgeInspect = useCallback((edgeData) => {
|
|
258
|
-
// Find the edge in the current edges to get the ID
|
|
259
|
-
const currentEdges = getEdgesForStep(currentStep);
|
|
260
|
-
const edge = currentEdges.find((e) => e.source === edgeData.source && e.target === edgeData.target);
|
|
261
|
-
const edgeWithId = {
|
|
262
|
-
...edgeData,
|
|
263
|
-
id: edge?.identifier || "N/A"
|
|
264
|
-
};
|
|
265
|
-
setSelectedData({ type: "edge", data: edgeWithId });
|
|
266
|
-
setIsDrawerOpen(true);
|
|
267
|
-
}, [getEdgesForStep, currentStep]);
|
|
268
|
-
// Convert flow data to ReactFlow format with step filtering
|
|
269
|
-
const nodes = useMemo(() => {
|
|
270
|
-
const stepNodes = getNodesForStep(currentStep);
|
|
271
|
-
const stepEdges = getEdgesForStep(currentStep);
|
|
272
|
-
return stepNodes.map((node) => {
|
|
273
|
-
const position = positions.get(node.identifier) || { x: 0, y: 0 };
|
|
274
|
-
const { description } = extractLLMDetails(node);
|
|
275
|
-
const isActive = node.stamp.step === currentStep;
|
|
276
|
-
return {
|
|
277
|
-
id: node.identifier,
|
|
278
|
-
type: "agent",
|
|
279
|
-
position,
|
|
280
|
-
data: {
|
|
281
|
-
label: node.node_type,
|
|
282
|
-
description,
|
|
283
|
-
nodeType: node.node_type,
|
|
284
|
-
step: node.stamp?.step,
|
|
285
|
-
time: node.stamp?.time,
|
|
286
|
-
isActive,
|
|
287
|
-
onInspect: handleNodeInspect,
|
|
288
|
-
id: node.identifier, // Add id for zoom functionality
|
|
289
|
-
edges: stepEdges // Pass edges to the node
|
|
290
|
-
},
|
|
291
|
-
style: {
|
|
292
|
-
filter: isActive ? "drop-shadow(0 4px 8px rgba(99, 102, 241, 0.3))" : "none"
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
});
|
|
296
|
-
}, [positions, currentStep, getNodesForStep, getEdgesForStep, handleNodeInspect]);
|
|
297
|
-
const edges = useMemo(() => {
|
|
298
|
-
const stepEdges = getEdgesForStep(currentStep);
|
|
299
|
-
return stepEdges
|
|
300
|
-
.filter((edge) => edge.source && edge.target)
|
|
301
|
-
.map((edge) => {
|
|
302
|
-
const isActive = edge.stamp.step === currentStep;
|
|
303
|
-
return {
|
|
304
|
-
id: edge.identifier,
|
|
305
|
-
type: "default",
|
|
306
|
-
source: edge.source,
|
|
307
|
-
target: edge.target,
|
|
308
|
-
animated: isActive,
|
|
309
|
-
bidirectional: true, // Enable bidirectional edges with arrow heads
|
|
310
|
-
style: {
|
|
311
|
-
stroke: isActive ? "#6366f1" : "#9ca3af",
|
|
312
|
-
strokeWidth: isActive ? 3 : 2
|
|
313
|
-
},
|
|
314
|
-
label: edge.stamp?.identifier ? truncateText(String(edge.stamp.identifier), 50) : undefined,
|
|
315
|
-
data: {
|
|
316
|
-
label: edge.stamp?.identifier ? truncateText(String(edge.stamp.identifier), 50) : undefined,
|
|
317
|
-
source: edge.source,
|
|
318
|
-
target: edge.target,
|
|
319
|
-
step: edge.stamp?.step,
|
|
320
|
-
time: edge.stamp?.time,
|
|
321
|
-
details: edge.details
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
});
|
|
325
|
-
}, [currentStep, getEdgesForStep]);
|
|
326
|
-
const [nodesState, setNodes, onNodesChange] = useNodesState(nodes);
|
|
327
|
-
const [edgesState, setEdges, onEdgesChange] = useEdgesState(edges);
|
|
328
|
-
// Update nodes and edges when currentStep changes
|
|
329
|
-
useEffect(() => {
|
|
330
|
-
setNodes(nodes);
|
|
331
|
-
}, [nodes, setNodes]);
|
|
332
|
-
useEffect(() => {
|
|
333
|
-
setEdges(edges);
|
|
334
|
-
}, [edges, setEdges]);
|
|
335
|
-
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
|
336
|
-
const handleStepChange = useCallback((step) => {
|
|
337
|
-
setCurrentStep(step);
|
|
338
|
-
setIsPlaying(false);
|
|
339
|
-
}, []);
|
|
340
|
-
const handlePlayPause = useCallback(() => {
|
|
341
|
-
setIsPlaying((prev) => !prev);
|
|
342
|
-
}, []);
|
|
343
|
-
// Function to convert client (screen) coordinates to SVG coordinates
|
|
344
|
-
const clientToSvgCoords = (clientX, clientY) => {
|
|
345
|
-
if (!svgRef.current)
|
|
346
|
-
return { x: 0, y: 0 };
|
|
347
|
-
const pt = svgRef.current.createSVGPoint();
|
|
348
|
-
pt.x = clientX;
|
|
349
|
-
pt.y = clientY;
|
|
350
|
-
const svgP = pt.matrixTransform(svgRef.current.getScreenCTM()?.inverse());
|
|
351
|
-
return { x: svgP.x, y: svgP.y };
|
|
352
|
-
};
|
|
353
|
-
const reactFlowInstance = useReactFlow();
|
|
354
|
-
// Pan to hub node (most connected node) on initial load
|
|
355
|
-
useEffect(() => {
|
|
356
|
-
if (nodes.length > 0 && reactFlowInstance) {
|
|
357
|
-
// Find the node with the most connections
|
|
358
|
-
const nodeConnectionCounts = new Map();
|
|
359
|
-
// Count incoming and outgoing connections for each node
|
|
360
|
-
edges.forEach((edge) => {
|
|
361
|
-
// Count outgoing connections (source)
|
|
362
|
-
const sourceCount = nodeConnectionCounts.get(edge.source) || 0;
|
|
363
|
-
nodeConnectionCounts.set(edge.source, sourceCount + 1);
|
|
364
|
-
// Count incoming connections (target)
|
|
365
|
-
const targetCount = nodeConnectionCounts.get(edge.target) || 0;
|
|
366
|
-
nodeConnectionCounts.set(edge.target, targetCount + 1);
|
|
367
|
-
});
|
|
368
|
-
// Find the node with the highest connection count
|
|
369
|
-
let hubNodeId = "";
|
|
370
|
-
let maxConnections = 0;
|
|
371
|
-
nodeConnectionCounts.forEach((count, nodeId) => {
|
|
372
|
-
if (count > maxConnections) {
|
|
373
|
-
maxConnections = count;
|
|
374
|
-
hubNodeId = nodeId;
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
// If no hub node found (no edges), use the first node
|
|
378
|
-
if (!hubNodeId && nodes.length > 0) {
|
|
379
|
-
hubNodeId = nodes[0].id;
|
|
380
|
-
}
|
|
381
|
-
// Pan to the hub node
|
|
382
|
-
if (hubNodeId) {
|
|
383
|
-
setTimeout(() => {
|
|
384
|
-
reactFlowInstance.fitView({
|
|
385
|
-
nodes: [{ id: hubNodeId }],
|
|
386
|
-
duration: 1000,
|
|
387
|
-
padding: 0.2,
|
|
388
|
-
minZoom: 0.2,
|
|
389
|
-
maxZoom: 1.5
|
|
390
|
-
});
|
|
391
|
-
}, 500); // Small delay to ensure nodes are rendered
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}, [nodes, edges, reactFlowInstance]);
|
|
395
|
-
return (_jsxs("div", { ref: containerRef, style: {
|
|
396
|
-
width: typeof width === "number" ? `${width}px` : width,
|
|
397
|
-
height: typeof height === "number" ? `${height}px` : height,
|
|
398
|
-
border: "1px solid #e5e7eb",
|
|
399
|
-
borderRadius: "8px",
|
|
400
|
-
overflow: "hidden",
|
|
401
|
-
position: "relative",
|
|
402
|
-
minWidth: "800px",
|
|
403
|
-
minHeight: "600px"
|
|
404
|
-
}, className: className, children: [_jsxs("div", { className: `side-panel ${showTimelines ? "expanded" : "collapsed"}`, children: [!showTimelines && (_jsx("button", { className: "panel-toggle", onClick: () => setShowTimelines(!showTimelines), title: "Expand Panel", children: _jsx(PanelLeft, { size: 20 }) })), showTimelines && (_jsx("div", { className: "panel-content", children: _jsx(VerticalTimeline, { stamps: flowData.stamps || flowData.steps || [], currentStep: currentStep, onStepChange: handleStepChange, onToggle: () => setShowTimelines(false) }) }))] }), _jsxs(ReactFlow, { nodes: nodesState, edges: edgesState, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onConnect: onConnect, nodeTypes: nodeTypes, edgeTypes: {
|
|
405
|
-
default: (edgeProps) => (_jsx(RCEdge, { ...edgeProps, clientToSvgCoords: clientToSvgCoords, svgRef: svgRef, onInspect: handleEdgeInspect }))
|
|
406
|
-
}, attributionPosition: "bottom-left", style: {
|
|
407
|
-
width: showTimelines ? "calc(100% - 280px)" : "100%", // Account for side panel width when visible
|
|
408
|
-
height: "calc(100% - 60px)", // Account for timeline height
|
|
409
|
-
marginLeft: showTimelines ? "280px" : "0" // Push content to the right of side panel when visible
|
|
410
|
-
}, defaultViewport: { x: 0, y: 0, zoom: 1 }, onInit: (instance) => {
|
|
411
|
-
if (containerRef.current) {
|
|
412
|
-
const svg = containerRef.current.querySelector("svg");
|
|
413
|
-
if (svg)
|
|
414
|
-
svgRef.current = svg;
|
|
415
|
-
}
|
|
416
|
-
}, children: [_jsx(Controls, {}), _jsx(Background, { color: "#f3f4f6", gap: 16 })] }), _jsxs("div", { className: `right-drawer ${isDrawerOpen ? "open" : ""}`, children: [_jsx("div", { className: "drawer-toggle", onClick: () => setIsDrawerOpen(!isDrawerOpen), children: _jsx(PanelRight, { size: 20 }) }), isDrawerOpen && selectedData && (_jsxs("div", { className: "drawer-content", children: [_jsx("div", { className: "drawer-header", children: _jsx("h3", { children: selectedData.type === "node" ? "Node Details" : "Edge Details" }) }), _jsx("div", { className: "drawer-body", children: selectedData.type === "node" ? (
|
|
417
|
-
// Node Details
|
|
418
|
-
_jsxs(_Fragment, { children: [_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Label:" }), _jsx("span", { className: "detail-value", children: selectedData.data.label || "N/A" })] }), selectedData.data.description && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Description:" }), _jsx("span", { className: "detail-value", children: selectedData.data.description })] })), selectedData.data.nodeType && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Type:" }), _jsx("span", { className: "detail-value", children: selectedData.data.nodeType })] })), selectedData.data.step && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Step:" }), _jsx("span", { className: "detail-value", children: selectedData.data.step })] })), selectedData.data.time && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Time:" }), _jsx("span", { className: "detail-value", children: new Date(selectedData.data.time * 1000).toLocaleString() })] })), selectedData.data.icon && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Icon:" }), _jsx("span", { className: "detail-value", children: selectedData.data.icon })] }))] })) : (
|
|
419
|
-
// Edge Details
|
|
420
|
-
_jsxs(_Fragment, { children: [_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "ID:" }), _jsx("span", { className: "detail-value", children: selectedData.data.id || "N/A" })] }), selectedData.data.source && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Source:" }), _jsx("span", { className: "detail-value", children: selectedData.data.source })] })), selectedData.data.target && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Target:" }), _jsx("span", { className: "detail-value", children: selectedData.data.target })] })), selectedData.data.label && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Label:" }), _jsx("span", { className: "detail-value", children: selectedData.data.label })] })), selectedData.data.step && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Step:" }), _jsx("span", { className: "detail-value", children: selectedData.data.step })] })), selectedData.data.time && (_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Time:" }), _jsx("span", { className: "detail-value", children: new Date(selectedData.data.time * 1000).toLocaleString() })] })), selectedData.data?.details?.input_args &&
|
|
421
|
-
Array.isArray(selectedData.data.details.input_args) &&
|
|
422
|
-
selectedData.data.details.input_args.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Inputs" }), _jsx("span", { className: "detail-value", style: { overflowY: "auto", maxHeight: "300px" }, children: Array.isArray(selectedData.data.details.input_args[0]) ? (selectedData.data.details.input_args[0].map((arg, index) => (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("span", { className: "detail-label", children: "Role:" }), _jsx("span", { className: "detail-value", children: arg?.role || "Unknown" }), _jsx("span", { className: "detail-label", children: "Content:" }), _jsx("span", { className: "detail-value", children: arg?.content || "No content" })] }, arg?.role || index)))) : (_jsx("span", { className: "detail-value", children: JSON.stringify(selectedData.data.details.input_args[0], null, 2) })) })] }), _jsxs("div", { className: "detail-row", children: [_jsx("span", { className: "detail-label", children: "Outputs" }), _jsx("span", { className: "detail-value", children: JSON.stringify(selectedData.data?.details?.output, null, 2) })] })] }))] })) })] }))] }), showTimelines && (_jsx("div", { style: { marginLeft: "280px", marginTop: "100px" }, children: _jsx(Timeline, { stamps: flowData.stamps || flowData.steps || [], currentStep: currentStep, isPlaying: isPlaying, onStepChange: handleStepChange, onPlayPause: handlePlayPause }) })), _jsx("style", { children: `
|
|
423
|
-
.react-flow__edge-label {
|
|
424
|
-
font-size: 10px;
|
|
425
|
-
background: white;
|
|
426
|
-
padding: 2px 4px;
|
|
427
|
-
border-radius: 4px;
|
|
428
|
-
border: 1px solid #e5e7eb;
|
|
429
|
-
max-width: 150px;
|
|
430
|
-
overflow: hidden;
|
|
431
|
-
text-overflow: ellipsis;
|
|
432
|
-
white-space: nowrap;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/* Right Drawer Styles */
|
|
436
|
-
.right-drawer {
|
|
437
|
-
position: absolute;
|
|
438
|
-
top: 0;
|
|
439
|
-
right: 0;
|
|
440
|
-
height: 100%;
|
|
441
|
-
z-index: 1000;
|
|
442
|
-
display: flex;
|
|
443
|
-
align-items: flex-start;
|
|
444
|
-
transition: transform 0.3s ease;
|
|
445
|
-
margin-left: ${showTimelines ? "280px" : "0"}; /* Account for vertical timeline when visible */
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
.right-drawer:not(.open) {
|
|
449
|
-
transform: translateX(calc(100% - 50px));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
.right-drawer.open {
|
|
453
|
-
transform: translateX(0);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
.drawer-toggle {
|
|
457
|
-
width: 50px;
|
|
458
|
-
height: 50px;
|
|
459
|
-
background: none;
|
|
460
|
-
color: #6b7280;
|
|
461
|
-
border: none;
|
|
462
|
-
cursor: pointer;
|
|
463
|
-
display: flex;
|
|
464
|
-
align-items: center;
|
|
465
|
-
justify-content: center;
|
|
466
|
-
transition: color 0.2s ease;
|
|
467
|
-
margin-top: 20px;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
.drawer-toggle:hover {
|
|
471
|
-
color: #374151;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
.drawer-content {
|
|
475
|
-
background: white;
|
|
476
|
-
border: 1px solid #e5e7eb;
|
|
477
|
-
border-radius: 8px 0 0 8px;
|
|
478
|
-
width: 400px;
|
|
479
|
-
height: calc(100% - 40px);
|
|
480
|
-
margin-top: 20px;
|
|
481
|
-
display: flex;
|
|
482
|
-
flex-direction: column;
|
|
483
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
484
|
-
animation: drawerSlideIn 0.3s ease-out;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
.drawer-header {
|
|
488
|
-
padding: 16px 20px;
|
|
489
|
-
border-bottom: 1px solid #e5e7eb;
|
|
490
|
-
background: #f9fafb;
|
|
491
|
-
display: flex;
|
|
492
|
-
justify-content: space-between;
|
|
493
|
-
align-items: center;
|
|
494
|
-
flex-shrink: 0;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
.drawer-header h3 {
|
|
498
|
-
margin: 0;
|
|
499
|
-
font-size: 16px;
|
|
500
|
-
font-weight: 600;
|
|
501
|
-
color: #1f2937;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
.close-button {
|
|
505
|
-
background: none;
|
|
506
|
-
border: none;
|
|
507
|
-
font-size: 20px;
|
|
508
|
-
color: #6b7280;
|
|
509
|
-
cursor: pointer;
|
|
510
|
-
padding: 4px;
|
|
511
|
-
width: 28px;
|
|
512
|
-
height: 28px;
|
|
513
|
-
display: flex;
|
|
514
|
-
align-items: center;
|
|
515
|
-
justify-content: center;
|
|
516
|
-
border-radius: 6px;
|
|
517
|
-
transition: all 0.2s ease;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
.close-button:hover {
|
|
521
|
-
background: #e5e7eb;
|
|
522
|
-
color: #1f2937;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
.drawer-body {
|
|
526
|
-
padding: 20px;
|
|
527
|
-
overflow-y: auto;
|
|
528
|
-
flex: 1;
|
|
529
|
-
width: 100%;
|
|
530
|
-
box-sizing: border-box;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
.detail-row {
|
|
534
|
-
display: grid;
|
|
535
|
-
grid-template-columns: 100px 1fr;
|
|
536
|
-
margin-bottom: 12px;
|
|
537
|
-
align-items: flex-start;
|
|
538
|
-
gap: 8px;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
.detail-row:last-child {
|
|
542
|
-
margin-bottom: 0;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
.detail-label {
|
|
546
|
-
font-weight: 600;
|
|
547
|
-
color: #6b7280;
|
|
548
|
-
font-size: 13px;
|
|
549
|
-
word-break: break-word;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
.detail-value {
|
|
553
|
-
color: #1f2937;
|
|
554
|
-
font-size: 13px;
|
|
555
|
-
word-break: break-word;
|
|
556
|
-
flex: 1;
|
|
557
|
-
width: 100%;
|
|
558
|
-
overflow: visible;
|
|
559
|
-
text-overflow: unset;
|
|
560
|
-
max-width: unset;
|
|
561
|
-
white-space: pre-line;
|
|
562
|
-
line-height: 1.4;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
@keyframes drawerSlideIn {
|
|
566
|
-
from {
|
|
567
|
-
opacity: 0;
|
|
568
|
-
transform: translateX(20px);
|
|
569
|
-
}
|
|
570
|
-
to {
|
|
571
|
-
opacity: 1;
|
|
572
|
-
transform: translateX(0);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/* Scoreboard Styles */
|
|
577
|
-
.scoreboard {
|
|
578
|
-
position: absolute;
|
|
579
|
-
top: 20px;
|
|
580
|
-
left: 50%;
|
|
581
|
-
transform: translateX(-50%);
|
|
582
|
-
background: white;
|
|
583
|
-
border: 1px solid #e5e7eb;
|
|
584
|
-
border-radius: 8px;
|
|
585
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
586
|
-
z-index: 1000;
|
|
587
|
-
min-width: 400px;
|
|
588
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
.scoreboard-header {
|
|
592
|
-
padding: 12px 16px;
|
|
593
|
-
border-bottom: 1px solid #e5e7eb;
|
|
594
|
-
background: #f9fafb;
|
|
595
|
-
text-align: center;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
.scoreboard-header h3 {
|
|
599
|
-
margin: 0;
|
|
600
|
-
font-size: 14px;
|
|
601
|
-
font-weight: 600;
|
|
602
|
-
color: #1f2937;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
.scoreboard-content {
|
|
606
|
-
padding: 16px;
|
|
607
|
-
display: flex;
|
|
608
|
-
justify-content: space-around;
|
|
609
|
-
align-items: center;
|
|
610
|
-
gap: 20px;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
.scoreboard-item {
|
|
614
|
-
display: flex;
|
|
615
|
-
flex-direction: column;
|
|
616
|
-
align-items: center;
|
|
617
|
-
gap: 4px;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
.scoreboard-label {
|
|
621
|
-
font-size: 12px;
|
|
622
|
-
color: #6b7280;
|
|
623
|
-
font-weight: 500;
|
|
624
|
-
text-transform: uppercase;
|
|
625
|
-
letter-spacing: 0.5px;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
.scoreboard-count {
|
|
629
|
-
font-size: 18px;
|
|
630
|
-
font-weight: 700;
|
|
631
|
-
padding: 6px 12px;
|
|
632
|
-
border-radius: 6px;
|
|
633
|
-
min-width: 40px;
|
|
634
|
-
text-align: center;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
.scoreboard-count.open {
|
|
638
|
-
background: #fef3c7;
|
|
639
|
-
color: #92400e;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
.scoreboard-count.completed {
|
|
643
|
-
background: #d1fae5;
|
|
644
|
-
color: #065f46;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
.scoreboard-count.error {
|
|
648
|
-
background: #fee2e2;
|
|
649
|
-
color: #991b1b;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
.scoreboard-count.error.clickable {
|
|
653
|
-
transition: all 0.2s ease;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
.scoreboard-count.error.clickable:hover {
|
|
657
|
-
background: #fecaca;
|
|
658
|
-
transform: scale(1.05);
|
|
659
|
-
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/* Side Panel Styles */
|
|
663
|
-
.side-panel {
|
|
664
|
-
position: absolute;
|
|
665
|
-
top: 0;
|
|
666
|
-
left: 0;
|
|
667
|
-
height: 100%;
|
|
668
|
-
z-index: 1000;
|
|
669
|
-
display: flex;
|
|
670
|
-
flex-direction: column;
|
|
671
|
-
transition: all 0.3s ease;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
.side-panel.collapsed {
|
|
675
|
-
width: 60px;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
.side-panel.expanded {
|
|
679
|
-
width: 280px;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
.panel-toggle {
|
|
683
|
-
display: flex;
|
|
684
|
-
align-items: center;
|
|
685
|
-
justify-content: center;
|
|
686
|
-
border: none;
|
|
687
|
-
background: none;
|
|
688
|
-
cursor: pointer;
|
|
689
|
-
color: #6b7280;
|
|
690
|
-
transition: color 0.2s ease;
|
|
691
|
-
padding: 8px;
|
|
692
|
-
margin: 20px 0 0 0;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
.panel-toggle:hover {
|
|
696
|
-
color: #374151;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
.panel-content {
|
|
700
|
-
flex: 1;
|
|
701
|
-
background: white;
|
|
702
|
-
border-right: 1px solid #e5e7eb;
|
|
703
|
-
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
|
704
|
-
overflow: hidden;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
` })] }));
|
|
709
|
-
};
|
|
710
|
-
export default AgenticFlowVisualizer;
|