flock-core 0.5.0b50__py3-none-any.whl → 0.5.0b51__py3-none-any.whl
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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/dashboard/launcher.py +1 -1
- flock/frontend/README.md +678 -0
- flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
- flock/frontend/index.html +12 -0
- flock/frontend/package-lock.json +4347 -0
- flock/frontend/package.json +48 -0
- flock/frontend/src/App.tsx +79 -0
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
- flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
- flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
- flock/frontend/src/components/common/BuildInfo.tsx +39 -0
- flock/frontend/src/components/common/EmptyState.module.css +115 -0
- flock/frontend/src/components/common/EmptyState.tsx +128 -0
- flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
- flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
- flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
- flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
- flock/frontend/src/components/controls/PublishControl.css +547 -0
- flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
- flock/frontend/src/components/controls/PublishControl.tsx +432 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
- flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
- flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
- flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
- flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
- flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
- flock/frontend/src/components/details/tabs.test.tsx +1015 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
- flock/frontend/src/components/filters/FilterBar.module.css +29 -0
- flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
- flock/frontend/src/components/filters/FilterBar.tsx +33 -0
- flock/frontend/src/components/filters/FilterPills.module.css +79 -0
- flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
- flock/frontend/src/components/filters/FilterPills.tsx +67 -0
- flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
- flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
- flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
- flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
- flock/frontend/src/components/graph/AgentNode.tsx +322 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
- flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
- flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
- flock/frontend/src/components/graph/MessageNode.tsx +116 -0
- flock/frontend/src/components/graph/MiniMap.tsx +47 -0
- flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
- flock/frontend/src/components/layout/DashboardLayout.css +407 -0
- flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
- flock/frontend/src/components/layout/Header.module.css +88 -0
- flock/frontend/src/components/layout/Header.tsx +52 -0
- flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
- flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
- flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
- flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
- flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
- flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
- flock/frontend/src/components/modules/registerModules.ts +20 -0
- flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
- flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
- flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
- flock/frontend/src/components/settings/SettingsPanel.css +327 -0
- flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
- flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
- flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
- flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
- flock/frontend/src/hooks/useModulePersistence.ts +154 -0
- flock/frontend/src/hooks/useModules.ts +139 -0
- flock/frontend/src/hooks/usePersistence.ts +139 -0
- flock/frontend/src/main.tsx +13 -0
- flock/frontend/src/services/api.ts +213 -0
- flock/frontend/src/services/indexeddb.test.ts +793 -0
- flock/frontend/src/services/indexeddb.ts +794 -0
- flock/frontend/src/services/layout.test.ts +437 -0
- flock/frontend/src/services/layout.ts +146 -0
- flock/frontend/src/services/themeApplicator.ts +140 -0
- flock/frontend/src/services/themeService.ts +77 -0
- flock/frontend/src/services/websocket.test.ts +595 -0
- flock/frontend/src/services/websocket.ts +685 -0
- flock/frontend/src/store/filterStore.test.ts +242 -0
- flock/frontend/src/store/filterStore.ts +103 -0
- flock/frontend/src/store/graphStore.test.ts +186 -0
- flock/frontend/src/store/graphStore.ts +414 -0
- flock/frontend/src/store/moduleStore.test.ts +253 -0
- flock/frontend/src/store/moduleStore.ts +57 -0
- flock/frontend/src/store/settingsStore.ts +188 -0
- flock/frontend/src/store/streamStore.ts +68 -0
- flock/frontend/src/store/uiStore.test.ts +54 -0
- flock/frontend/src/store/uiStore.ts +110 -0
- flock/frontend/src/store/wsStore.ts +34 -0
- flock/frontend/src/styles/index.css +15 -0
- flock/frontend/src/styles/scrollbar.css +47 -0
- flock/frontend/src/styles/variables.css +488 -0
- flock/frontend/src/test/setup.ts +1 -0
- flock/frontend/src/types/filters.ts +14 -0
- flock/frontend/src/types/graph.ts +55 -0
- flock/frontend/src/types/modules.ts +7 -0
- flock/frontend/src/types/theme.ts +55 -0
- flock/frontend/src/utils/mockData.ts +85 -0
- flock/frontend/src/utils/performance.ts +16 -0
- flock/frontend/src/utils/transforms.test.ts +860 -0
- flock/frontend/src/utils/transforms.ts +323 -0
- flock/frontend/src/vite-env.d.ts +17 -0
- flock/frontend/tsconfig.json +27 -0
- flock/frontend/tsconfig.node.json +11 -0
- flock/frontend/vite.config.ts +25 -0
- flock/frontend/vitest.config.ts +11 -0
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/METADATA +1 -1
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/RECORD +116 -6
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { useCallback, useMemo, useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ReactFlow,
|
|
4
|
+
Background,
|
|
5
|
+
Controls,
|
|
6
|
+
NodeChange,
|
|
7
|
+
EdgeChange,
|
|
8
|
+
applyNodeChanges,
|
|
9
|
+
applyEdgeChanges,
|
|
10
|
+
useReactFlow,
|
|
11
|
+
type Node,
|
|
12
|
+
} from '@xyflow/react';
|
|
13
|
+
import '@xyflow/react/dist/style.css';
|
|
14
|
+
import AgentNode from './AgentNode';
|
|
15
|
+
import MessageNode from './MessageNode';
|
|
16
|
+
import MessageFlowEdge from './MessageFlowEdge';
|
|
17
|
+
import TransformEdge from './TransformEdge';
|
|
18
|
+
import MiniMap from './MiniMap';
|
|
19
|
+
import { useGraphStore } from '../../store/graphStore';
|
|
20
|
+
import { useUIStore } from '../../store/uiStore';
|
|
21
|
+
import { useModuleStore } from '../../store/moduleStore';
|
|
22
|
+
import { useSettingsStore } from '../../store/settingsStore';
|
|
23
|
+
import { moduleRegistry } from '../modules/ModuleRegistry';
|
|
24
|
+
import { applyDagreLayout } from '../../services/layout';
|
|
25
|
+
import { usePersistence } from '../../hooks/usePersistence';
|
|
26
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
27
|
+
|
|
28
|
+
const GraphCanvas: React.FC = () => {
|
|
29
|
+
const { fitView, getIntersectingNodes } = useReactFlow();
|
|
30
|
+
|
|
31
|
+
const mode = useUIStore((state) => state.mode);
|
|
32
|
+
const openDetailWindow = useUIStore((state) => state.openDetailWindow);
|
|
33
|
+
const layoutDirection = useSettingsStore((state) => state.advanced.layoutDirection);
|
|
34
|
+
const nodes = useGraphStore((state) => state.nodes);
|
|
35
|
+
const edges = useGraphStore((state) => state.edges);
|
|
36
|
+
const agents = useGraphStore((state) => state.agents);
|
|
37
|
+
const messages = useGraphStore((state) => state.messages);
|
|
38
|
+
const runs = useGraphStore((state) => state.runs);
|
|
39
|
+
const generateAgentViewGraph = useGraphStore((state) => state.generateAgentViewGraph);
|
|
40
|
+
const generateBlackboardViewGraph = useGraphStore((state) => state.generateBlackboardViewGraph);
|
|
41
|
+
const updateNodePosition = useGraphStore((state) => state.updateNodePosition);
|
|
42
|
+
const addModule = useModuleStore((state) => state.addModule);
|
|
43
|
+
|
|
44
|
+
// Graph settings from settings store
|
|
45
|
+
const edgeType = useSettingsStore((state) => state.graph.edgeType);
|
|
46
|
+
const edgeStrokeWidth = useSettingsStore((state) => state.graph.edgeStrokeWidth);
|
|
47
|
+
const edgeAnimation = useSettingsStore((state) => state.graph.edgeAnimation);
|
|
48
|
+
|
|
49
|
+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
50
|
+
const [showModuleSubmenu, setShowModuleSubmenu] = useState(false);
|
|
51
|
+
|
|
52
|
+
// Persistence hook - loads positions on mount and handles saves
|
|
53
|
+
const { saveNodePosition } = usePersistence();
|
|
54
|
+
|
|
55
|
+
// Memoize node types to prevent re-creation
|
|
56
|
+
const nodeTypes = useMemo(
|
|
57
|
+
() => ({
|
|
58
|
+
agent: AgentNode,
|
|
59
|
+
message: MessageNode,
|
|
60
|
+
}),
|
|
61
|
+
[]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Memoize edge types to prevent re-creation
|
|
65
|
+
const edgeTypes = useMemo(
|
|
66
|
+
() => ({
|
|
67
|
+
message_flow: MessageFlowEdge,
|
|
68
|
+
transformation: TransformEdge,
|
|
69
|
+
}),
|
|
70
|
+
[]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Generate graph when mode changes OR when agents/messages/runs change
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (mode === 'agent') {
|
|
76
|
+
generateAgentViewGraph();
|
|
77
|
+
} else {
|
|
78
|
+
generateBlackboardViewGraph();
|
|
79
|
+
}
|
|
80
|
+
}, [mode, agents, messages, runs, generateAgentViewGraph, generateBlackboardViewGraph]);
|
|
81
|
+
|
|
82
|
+
// Regenerate graph when edge settings change to apply new edge styles
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (mode === 'agent') {
|
|
85
|
+
generateAgentViewGraph();
|
|
86
|
+
} else {
|
|
87
|
+
generateBlackboardViewGraph();
|
|
88
|
+
}
|
|
89
|
+
}, [edgeType, edgeStrokeWidth, edgeAnimation, mode, generateAgentViewGraph, generateBlackboardViewGraph]);
|
|
90
|
+
|
|
91
|
+
const onNodesChange = useCallback(
|
|
92
|
+
(changes: NodeChange[]) => {
|
|
93
|
+
const updatedNodes = applyNodeChanges(changes, nodes);
|
|
94
|
+
useGraphStore.setState({ nodes: updatedNodes });
|
|
95
|
+
|
|
96
|
+
// Update position in store for persistence
|
|
97
|
+
changes.forEach((change) => {
|
|
98
|
+
if (change.type === 'position' && change.position) {
|
|
99
|
+
updateNodePosition(change.id, change.position);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
[nodes, updateNodePosition]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const onEdgesChange = useCallback(
|
|
107
|
+
(changes: EdgeChange[]) => {
|
|
108
|
+
const updatedEdges = applyEdgeChanges(changes, edges);
|
|
109
|
+
useGraphStore.setState({ edges: updatedEdges });
|
|
110
|
+
},
|
|
111
|
+
[edges]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Auto-layout handler
|
|
115
|
+
const handleAutoLayout = useCallback(() => {
|
|
116
|
+
const nodeSpacing = useSettingsStore.getState().advanced.nodeSpacing;
|
|
117
|
+
const rankSpacing = useSettingsStore.getState().advanced.rankSpacing;
|
|
118
|
+
const layoutedNodes = applyDagreLayout(nodes, edges, layoutDirection || 'TB', nodeSpacing, rankSpacing);
|
|
119
|
+
|
|
120
|
+
// Update nodes with new positions
|
|
121
|
+
layoutedNodes.forEach((node) => {
|
|
122
|
+
updateNodePosition(node.id, node.position);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
useGraphStore.setState({ nodes: layoutedNodes });
|
|
126
|
+
setContextMenu(null);
|
|
127
|
+
setShowModuleSubmenu(false);
|
|
128
|
+
}, [nodes, edges, layoutDirection, updateNodePosition]);
|
|
129
|
+
|
|
130
|
+
// Auto-zoom handler
|
|
131
|
+
const handleAutoZoom = useCallback(() => {
|
|
132
|
+
fitView({ padding: 0.1, duration: 300 });
|
|
133
|
+
setContextMenu(null);
|
|
134
|
+
setShowModuleSubmenu(false);
|
|
135
|
+
}, [fitView]);
|
|
136
|
+
|
|
137
|
+
// Add module handler
|
|
138
|
+
const handleAddModule = useCallback((moduleType: string, clickX: number, clickY: number) => {
|
|
139
|
+
const moduleInstance = {
|
|
140
|
+
id: uuidv4(),
|
|
141
|
+
type: moduleType,
|
|
142
|
+
position: { x: clickX, y: clickY },
|
|
143
|
+
size: { width: 600, height: 400 },
|
|
144
|
+
visible: true,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
addModule(moduleInstance);
|
|
148
|
+
setContextMenu(null);
|
|
149
|
+
setShowModuleSubmenu(false);
|
|
150
|
+
}, [addModule]);
|
|
151
|
+
|
|
152
|
+
// Context menu handler
|
|
153
|
+
const onPaneContextMenu = useCallback((event: React.MouseEvent | MouseEvent) => {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
setContextMenu({
|
|
156
|
+
x: event.clientX,
|
|
157
|
+
y: event.clientY,
|
|
158
|
+
});
|
|
159
|
+
setShowModuleSubmenu(false);
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
// Close context menu on click outside
|
|
163
|
+
const onPaneClick = useCallback(() => {
|
|
164
|
+
setContextMenu(null);
|
|
165
|
+
setShowModuleSubmenu(false);
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
// Node drag handler - prevent overlaps with collision detection
|
|
169
|
+
const onNodeDrag = useCallback(
|
|
170
|
+
(_event: React.MouseEvent | MouseEvent, node: Node) => {
|
|
171
|
+
const intersections = getIntersectingNodes(node);
|
|
172
|
+
|
|
173
|
+
// If there are intersecting nodes, snap back to prevent overlap
|
|
174
|
+
if (intersections.length > 0) {
|
|
175
|
+
// Revert to previous position by updating the nodes
|
|
176
|
+
useGraphStore.setState((state) => ({
|
|
177
|
+
nodes: state.nodes.map((n) =>
|
|
178
|
+
n.id === node.id
|
|
179
|
+
? { ...n, position: n.position } // Keep previous position
|
|
180
|
+
: n
|
|
181
|
+
),
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
[getIntersectingNodes]
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Node drag stop handler - persist position with 300ms debounce
|
|
189
|
+
const onNodeDragStop = useCallback(
|
|
190
|
+
(_event: React.MouseEvent | MouseEvent, node: Node) => {
|
|
191
|
+
saveNodePosition(node.id, node.position);
|
|
192
|
+
},
|
|
193
|
+
[saveNodePosition]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Node double-click handler - open detail window
|
|
197
|
+
const onNodeDoubleClick = useCallback(
|
|
198
|
+
(_event: React.MouseEvent, node: Node) => {
|
|
199
|
+
openDetailWindow(node.id);
|
|
200
|
+
},
|
|
201
|
+
[openDetailWindow]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const defaultEdgeOptions = useMemo(
|
|
205
|
+
() => ({
|
|
206
|
+
type: edgeType,
|
|
207
|
+
animated: edgeAnimation,
|
|
208
|
+
style: {
|
|
209
|
+
stroke: 'var(--color-edge-default)',
|
|
210
|
+
strokeWidth: edgeStrokeWidth,
|
|
211
|
+
},
|
|
212
|
+
}),
|
|
213
|
+
[edgeType, edgeAnimation, edgeStrokeWidth]
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div style={{ width: '100%', height: '100%', position: 'relative' }}>
|
|
218
|
+
<ReactFlow
|
|
219
|
+
nodes={nodes}
|
|
220
|
+
edges={edges}
|
|
221
|
+
onNodesChange={onNodesChange}
|
|
222
|
+
onEdgesChange={onEdgesChange}
|
|
223
|
+
onNodeDrag={onNodeDrag}
|
|
224
|
+
onNodeDragStop={onNodeDragStop}
|
|
225
|
+
onNodeDoubleClick={onNodeDoubleClick}
|
|
226
|
+
nodeTypes={nodeTypes}
|
|
227
|
+
edgeTypes={edgeTypes}
|
|
228
|
+
defaultEdgeOptions={defaultEdgeOptions}
|
|
229
|
+
onPaneContextMenu={onPaneContextMenu}
|
|
230
|
+
onPaneClick={onPaneClick}
|
|
231
|
+
style={{
|
|
232
|
+
backgroundColor: 'var(--color-bg-elevated)',
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
<Background
|
|
236
|
+
color="var(--color-border-subtle)"
|
|
237
|
+
gap={16}
|
|
238
|
+
size={1}
|
|
239
|
+
style={{
|
|
240
|
+
backgroundColor: 'var(--color-bg-elevated)',
|
|
241
|
+
}}
|
|
242
|
+
/>
|
|
243
|
+
<Controls
|
|
244
|
+
style={{
|
|
245
|
+
backgroundColor: 'var(--color-bg-surface)',
|
|
246
|
+
border: '1px solid var(--color-border-default)',
|
|
247
|
+
borderRadius: 'var(--radius-lg)',
|
|
248
|
+
overflow: 'hidden',
|
|
249
|
+
boxShadow: 'var(--shadow-lg)',
|
|
250
|
+
}}
|
|
251
|
+
showZoom={true}
|
|
252
|
+
showFitView={true}
|
|
253
|
+
showInteractive={true}
|
|
254
|
+
/>
|
|
255
|
+
<MiniMap />
|
|
256
|
+
</ReactFlow>
|
|
257
|
+
|
|
258
|
+
{/* Context Menu */}
|
|
259
|
+
{contextMenu && (
|
|
260
|
+
<div
|
|
261
|
+
style={{
|
|
262
|
+
position: 'fixed',
|
|
263
|
+
top: contextMenu.y,
|
|
264
|
+
left: contextMenu.x,
|
|
265
|
+
background: 'var(--color-bg-surface)',
|
|
266
|
+
border: 'var(--border-default)',
|
|
267
|
+
borderRadius: 'var(--radius-md)',
|
|
268
|
+
boxShadow: 'var(--shadow-lg)',
|
|
269
|
+
zIndex: 1000,
|
|
270
|
+
minWidth: 180,
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
<button
|
|
274
|
+
onClick={handleAutoLayout}
|
|
275
|
+
style={{
|
|
276
|
+
display: 'block',
|
|
277
|
+
width: '100%',
|
|
278
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
279
|
+
border: 'none',
|
|
280
|
+
background: 'transparent',
|
|
281
|
+
cursor: 'pointer',
|
|
282
|
+
textAlign: 'left',
|
|
283
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
284
|
+
color: 'var(--color-text-primary)',
|
|
285
|
+
transition: 'var(--transition-colors)',
|
|
286
|
+
}}
|
|
287
|
+
onMouseEnter={(e) => {
|
|
288
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
289
|
+
}}
|
|
290
|
+
onMouseLeave={(e) => {
|
|
291
|
+
e.currentTarget.style.background = 'transparent';
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
Auto Layout
|
|
295
|
+
</button>
|
|
296
|
+
|
|
297
|
+
<button
|
|
298
|
+
onClick={handleAutoZoom}
|
|
299
|
+
style={{
|
|
300
|
+
display: 'block',
|
|
301
|
+
width: '100%',
|
|
302
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
303
|
+
border: 'none',
|
|
304
|
+
background: 'transparent',
|
|
305
|
+
cursor: 'pointer',
|
|
306
|
+
textAlign: 'left',
|
|
307
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
308
|
+
color: 'var(--color-text-primary)',
|
|
309
|
+
transition: 'var(--transition-colors)',
|
|
310
|
+
}}
|
|
311
|
+
onMouseEnter={(e) => {
|
|
312
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
313
|
+
}}
|
|
314
|
+
onMouseLeave={(e) => {
|
|
315
|
+
e.currentTarget.style.background = 'transparent';
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
Auto Zoom
|
|
319
|
+
</button>
|
|
320
|
+
|
|
321
|
+
<div style={{ position: 'relative' }}>
|
|
322
|
+
<button
|
|
323
|
+
onMouseEnter={() => setShowModuleSubmenu(true)}
|
|
324
|
+
onMouseLeave={(e) => {
|
|
325
|
+
// Only close submenu if not moving to submenu itself
|
|
326
|
+
const relatedTarget = e.relatedTarget as HTMLElement;
|
|
327
|
+
if (!relatedTarget || !relatedTarget.closest('.module-submenu')) {
|
|
328
|
+
setShowModuleSubmenu(false);
|
|
329
|
+
}
|
|
330
|
+
}}
|
|
331
|
+
style={{
|
|
332
|
+
display: 'flex',
|
|
333
|
+
alignItems: 'center',
|
|
334
|
+
justifyContent: 'space-between',
|
|
335
|
+
width: '100%',
|
|
336
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
337
|
+
border: 'none',
|
|
338
|
+
background: showModuleSubmenu ? 'var(--color-bg-overlay)' : 'transparent',
|
|
339
|
+
cursor: 'pointer',
|
|
340
|
+
textAlign: 'left',
|
|
341
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
342
|
+
color: 'var(--color-text-primary)',
|
|
343
|
+
transition: 'var(--transition-colors)',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
<span>Add Module</span>
|
|
347
|
+
<span style={{ marginLeft: 'var(--spacing-2)' }}>▶</span>
|
|
348
|
+
</button>
|
|
349
|
+
|
|
350
|
+
{/* Module Submenu */}
|
|
351
|
+
{showModuleSubmenu && (
|
|
352
|
+
<div
|
|
353
|
+
className="module-submenu"
|
|
354
|
+
onMouseEnter={() => setShowModuleSubmenu(true)}
|
|
355
|
+
onMouseLeave={() => setShowModuleSubmenu(false)}
|
|
356
|
+
style={{
|
|
357
|
+
position: 'absolute',
|
|
358
|
+
top: 0,
|
|
359
|
+
left: '100%',
|
|
360
|
+
background: 'var(--color-bg-surface)',
|
|
361
|
+
border: 'var(--border-default)',
|
|
362
|
+
borderRadius: 'var(--radius-md)',
|
|
363
|
+
boxShadow: 'var(--shadow-lg)',
|
|
364
|
+
zIndex: 1001,
|
|
365
|
+
minWidth: 160,
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
{moduleRegistry.getAll().map((module) => (
|
|
369
|
+
<button
|
|
370
|
+
key={module.id}
|
|
371
|
+
onClick={() => handleAddModule(module.id, contextMenu.x, contextMenu.y)}
|
|
372
|
+
style={{
|
|
373
|
+
display: 'flex',
|
|
374
|
+
alignItems: 'center',
|
|
375
|
+
gap: 'var(--gap-sm)',
|
|
376
|
+
width: '100%',
|
|
377
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
378
|
+
border: 'none',
|
|
379
|
+
background: 'transparent',
|
|
380
|
+
cursor: 'pointer',
|
|
381
|
+
textAlign: 'left',
|
|
382
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
383
|
+
color: 'var(--color-text-primary)',
|
|
384
|
+
transition: 'var(--transition-colors)',
|
|
385
|
+
}}
|
|
386
|
+
onMouseEnter={(e) => {
|
|
387
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
388
|
+
}}
|
|
389
|
+
onMouseLeave={(e) => {
|
|
390
|
+
e.currentTarget.style.background = 'transparent';
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
{module.icon && <span>{module.icon}</span>}
|
|
394
|
+
<span>{module.name}</span>
|
|
395
|
+
</button>
|
|
396
|
+
))}
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
export default GraphCanvas;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BaseEdge,
|
|
4
|
+
EdgeProps,
|
|
5
|
+
getBezierPath,
|
|
6
|
+
getStraightPath,
|
|
7
|
+
getSmoothStepPath,
|
|
8
|
+
getSimpleBezierPath,
|
|
9
|
+
EdgeLabelRenderer
|
|
10
|
+
} from '@xyflow/react';
|
|
11
|
+
import { useSettingsStore } from '../../store/settingsStore';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Phase 4: Graph Visualization & Dual Views - MessageFlowEdge Component
|
|
15
|
+
*
|
|
16
|
+
* Custom edge for Agent View showing message flow between agents.
|
|
17
|
+
* Features:
|
|
18
|
+
* - Animated edge to show active message flow
|
|
19
|
+
* - Label with message type and count (e.g., "Movie (3)")
|
|
20
|
+
* - Arrow marker at target
|
|
21
|
+
*
|
|
22
|
+
* SPECIFICATION: docs/specs/003-real-time-dashboard/PLAN.md Phase 4
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface MessageFlowEdgeData {
|
|
26
|
+
messageType: string;
|
|
27
|
+
messageCount: number;
|
|
28
|
+
artifactIds: string[];
|
|
29
|
+
latestTimestamp: string;
|
|
30
|
+
labelOffset?: number; // Phase 11: Vertical offset to prevent label overlap
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MessageFlowEdge: React.FC<EdgeProps> = ({
|
|
34
|
+
id,
|
|
35
|
+
sourceX,
|
|
36
|
+
sourceY,
|
|
37
|
+
targetX,
|
|
38
|
+
targetY,
|
|
39
|
+
sourcePosition,
|
|
40
|
+
targetPosition,
|
|
41
|
+
label,
|
|
42
|
+
style = {},
|
|
43
|
+
markerEnd,
|
|
44
|
+
data,
|
|
45
|
+
}) => {
|
|
46
|
+
// Get edge settings from settings store
|
|
47
|
+
const edgeType = useSettingsStore((state) => state.graph.edgeType);
|
|
48
|
+
const edgeStrokeWidth = useSettingsStore((state) => state.graph.edgeStrokeWidth);
|
|
49
|
+
const edgeAnimation = useSettingsStore((state) => state.graph.edgeAnimation);
|
|
50
|
+
const showEdgeLabels = useSettingsStore((state) => state.graph.showEdgeLabels);
|
|
51
|
+
|
|
52
|
+
// Use appropriate path function based on settings
|
|
53
|
+
const getPath = () => {
|
|
54
|
+
const params = { sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition };
|
|
55
|
+
switch (edgeType) {
|
|
56
|
+
case 'straight':
|
|
57
|
+
return getStraightPath(params);
|
|
58
|
+
case 'smoothstep':
|
|
59
|
+
return getSmoothStepPath(params);
|
|
60
|
+
case 'simplebezier':
|
|
61
|
+
return getSimpleBezierPath(params);
|
|
62
|
+
case 'bezier':
|
|
63
|
+
default:
|
|
64
|
+
return getBezierPath(params);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const [edgePath, labelX, labelY] = getPath();
|
|
69
|
+
|
|
70
|
+
// Phase 11 Bug Fix: Apply label offset to prevent overlap when multiple edges exist
|
|
71
|
+
const edgeData = data as MessageFlowEdgeData | undefined;
|
|
72
|
+
const labelOffset = edgeData?.labelOffset || 0;
|
|
73
|
+
|
|
74
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
<BaseEdge
|
|
79
|
+
id={id}
|
|
80
|
+
path={edgePath}
|
|
81
|
+
style={{
|
|
82
|
+
...style,
|
|
83
|
+
stroke: 'var(--color-edge-message)',
|
|
84
|
+
strokeWidth: isHovered ? edgeStrokeWidth + 1 : edgeStrokeWidth,
|
|
85
|
+
animation: edgeAnimation ? 'dash 20s linear infinite' : 'none',
|
|
86
|
+
transition: 'var(--transition-all)',
|
|
87
|
+
filter: isHovered ? 'drop-shadow(0 0 4px var(--color-edge-message))' : 'none',
|
|
88
|
+
}}
|
|
89
|
+
markerEnd={markerEnd}
|
|
90
|
+
/>
|
|
91
|
+
{label && showEdgeLabels && (
|
|
92
|
+
<EdgeLabelRenderer>
|
|
93
|
+
<div
|
|
94
|
+
style={{
|
|
95
|
+
position: 'absolute',
|
|
96
|
+
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY + labelOffset}px)`,
|
|
97
|
+
fontSize: 'var(--font-size-caption)',
|
|
98
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
99
|
+
background: 'var(--color-edge-label-bg)',
|
|
100
|
+
color: 'var(--color-edge-label-text)',
|
|
101
|
+
padding: 'var(--spacing-1) var(--spacing-2)',
|
|
102
|
+
borderRadius: 'var(--radius-sm)',
|
|
103
|
+
border: 'var(--border-width-1) solid var(--color-edge-message)',
|
|
104
|
+
pointerEvents: 'all',
|
|
105
|
+
backdropFilter: 'blur(var(--blur-sm))',
|
|
106
|
+
boxShadow: 'var(--shadow-sm)',
|
|
107
|
+
transition: 'var(--transition-all)',
|
|
108
|
+
}}
|
|
109
|
+
className="nodrag nopan"
|
|
110
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
111
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
112
|
+
>
|
|
113
|
+
{label}
|
|
114
|
+
</div>
|
|
115
|
+
</EdgeLabelRenderer>
|
|
116
|
+
)}
|
|
117
|
+
<style>{`
|
|
118
|
+
@keyframes dash {
|
|
119
|
+
to {
|
|
120
|
+
stroke-dashoffset: -100;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
`}</style>
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export default MessageFlowEdge;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { ReactFlowProvider } from '@xyflow/react';
|
|
4
|
+
import MessageNode from './MessageNode';
|
|
5
|
+
import { MessageNodeData } from '../../types/graph';
|
|
6
|
+
import { NodeProps } from '@xyflow/react';
|
|
7
|
+
|
|
8
|
+
describe('MessageNode', () => {
|
|
9
|
+
const createNodeProps = (data: MessageNodeData, selected = false): NodeProps =>
|
|
10
|
+
({
|
|
11
|
+
id: 'msg-1',
|
|
12
|
+
data,
|
|
13
|
+
selected,
|
|
14
|
+
type: 'message',
|
|
15
|
+
isConnectable: true,
|
|
16
|
+
dragging: false,
|
|
17
|
+
zIndex: 0,
|
|
18
|
+
selectable: true,
|
|
19
|
+
deletable: true,
|
|
20
|
+
draggable: true,
|
|
21
|
+
}) as unknown as NodeProps;
|
|
22
|
+
|
|
23
|
+
it('should render artifact type', () => {
|
|
24
|
+
const data: MessageNodeData = {
|
|
25
|
+
artifactType: 'Movie',
|
|
26
|
+
payloadPreview: '{"title": "Test Movie"}',
|
|
27
|
+
payload: { title: 'Test Movie' },
|
|
28
|
+
producedBy: 'movie',
|
|
29
|
+
consumedBy: ['tagline'],
|
|
30
|
+
timestamp: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
render(
|
|
34
|
+
<ReactFlowProvider>
|
|
35
|
+
<MessageNode {...createNodeProps(data)} />
|
|
36
|
+
</ReactFlowProvider>
|
|
37
|
+
);
|
|
38
|
+
expect(screen.getByText('Movie')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should render produced by', () => {
|
|
42
|
+
const data: MessageNodeData = {
|
|
43
|
+
artifactType: 'Movie',
|
|
44
|
+
payloadPreview: '{"title": "Test Movie"}',
|
|
45
|
+
payload: { title: 'Test Movie' },
|
|
46
|
+
producedBy: 'movie',
|
|
47
|
+
consumedBy: [],
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
render(
|
|
52
|
+
<ReactFlowProvider>
|
|
53
|
+
<MessageNode {...createNodeProps(data)} />
|
|
54
|
+
</ReactFlowProvider>
|
|
55
|
+
);
|
|
56
|
+
// Text is split across elements: <div>by: <span>movie</span></div>
|
|
57
|
+
// Use getByText with function matcher to find text across elements
|
|
58
|
+
expect(screen.getByText((_content, element) => {
|
|
59
|
+
return element?.textContent === 'by: movie';
|
|
60
|
+
})).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
});
|