flock-core 0.5.0b71__py3-none-any.whl → 0.5.0b75__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/agent.py +39 -1
- flock/artifacts.py +17 -10
- flock/cli.py +1 -1
- flock/dashboard/__init__.py +2 -0
- flock/dashboard/collector.py +282 -6
- flock/dashboard/events.py +6 -0
- flock/dashboard/graph_builder.py +563 -0
- flock/dashboard/launcher.py +11 -6
- flock/dashboard/models/graph.py +156 -0
- flock/dashboard/service.py +175 -14
- flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
- flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
- flock/dashboard/static_v2/index.html +13 -0
- flock/dashboard/websocket.py +2 -2
- flock/engines/dspy_engine.py +27 -8
- flock/frontend/README.md +6 -6
- flock/frontend/src/App.tsx +23 -31
- flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
- flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
- flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
- flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
- flock/frontend/src/components/graph/AgentNode.tsx +8 -6
- flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
- flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
- flock/frontend/src/components/graph/MessageNode.tsx +16 -3
- flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
- flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
- flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
- flock/frontend/src/hooks/useModules.ts +12 -4
- flock/frontend/src/hooks/usePersistence.ts +5 -3
- flock/frontend/src/services/api.ts +3 -19
- flock/frontend/src/services/graphService.test.ts +330 -0
- flock/frontend/src/services/graphService.ts +75 -0
- flock/frontend/src/services/websocket.ts +104 -268
- flock/frontend/src/store/filterStore.test.ts +89 -1
- flock/frontend/src/store/filterStore.ts +38 -16
- flock/frontend/src/store/graphStore.test.ts +538 -173
- flock/frontend/src/store/graphStore.ts +374 -465
- flock/frontend/src/store/moduleStore.ts +51 -33
- flock/frontend/src/store/uiStore.ts +23 -11
- flock/frontend/src/types/graph.ts +77 -44
- flock/frontend/src/utils/mockData.ts +16 -3
- flock/frontend/vite.config.ts +2 -2
- flock/orchestrator.py +24 -6
- flock/service.py +2 -2
- flock/store.py +169 -4
- flock/themes/darkmatrix.toml +2 -2
- flock/themes/deep.toml +2 -2
- flock/themes/neopolitan.toml +4 -4
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/METADATA +1 -1
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/RECORD +56 -53
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
- flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
- flock/frontend/src/services/websocket.test.ts +0 -595
- flock/frontend/src/utils/transforms.test.ts +0 -860
- flock/frontend/src/utils/transforms.ts +0 -323
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { fetchGraphSnapshot, mergeNodePositions, overlayWebSocketState } from './graphService';
|
|
3
|
+
import { GraphRequest, GraphSnapshot } from '../types/graph';
|
|
4
|
+
import { Node } from '@xyflow/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Graph Service Tests - UI Optimization Migration (Spec 002)
|
|
8
|
+
*
|
|
9
|
+
* Tests the NEW graph service layer that replaces client-side graph construction
|
|
10
|
+
* with backend snapshot consumption.
|
|
11
|
+
*
|
|
12
|
+
* SPECIFICATION: docs/internal/ui-optimization/03-migration-implementation-guide.md
|
|
13
|
+
* FOCUS: Backend integration, position merging, WebSocket state overlay, error handling
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
describe('graphService', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('fetchGraphSnapshot', () => {
|
|
26
|
+
it('should fetch agent view graph from backend with correct request format', async () => {
|
|
27
|
+
const mockSnapshot: GraphSnapshot = {
|
|
28
|
+
nodes: [
|
|
29
|
+
{ id: 'agent1', type: 'agent', data: { name: 'agent1' }, position: { x: 0, y: 0 }, hidden: false },
|
|
30
|
+
],
|
|
31
|
+
edges: [
|
|
32
|
+
{ id: 'edge1', source: 'agent1', target: 'agent2', type: 'message_flow', hidden: false, data: {} },
|
|
33
|
+
],
|
|
34
|
+
statistics: null,
|
|
35
|
+
viewMode: 'agent',
|
|
36
|
+
filters: {
|
|
37
|
+
correlation_id: null,
|
|
38
|
+
time_range: { preset: 'last10min' },
|
|
39
|
+
artifactTypes: [],
|
|
40
|
+
producers: [],
|
|
41
|
+
tags: [],
|
|
42
|
+
visibility: [],
|
|
43
|
+
},
|
|
44
|
+
generatedAt: '2025-10-11T00:00:00Z',
|
|
45
|
+
totalArtifacts: 1,
|
|
46
|
+
truncated: false,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
50
|
+
ok: true,
|
|
51
|
+
json: async () => mockSnapshot,
|
|
52
|
+
}) as any;
|
|
53
|
+
|
|
54
|
+
const request: GraphRequest = {
|
|
55
|
+
viewMode: 'agent',
|
|
56
|
+
filters: {
|
|
57
|
+
correlation_id: null,
|
|
58
|
+
time_range: { preset: 'last10min' },
|
|
59
|
+
artifactTypes: ['Pizza'],
|
|
60
|
+
producers: ['pizza_master'],
|
|
61
|
+
tags: [],
|
|
62
|
+
visibility: [],
|
|
63
|
+
},
|
|
64
|
+
options: { include_statistics: true },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const result = await fetchGraphSnapshot(request);
|
|
68
|
+
|
|
69
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/dashboard/graph', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify(request),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual(mockSnapshot);
|
|
76
|
+
expect(result.nodes).toHaveLength(1);
|
|
77
|
+
expect(result.edges).toHaveLength(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should throw error when API call fails', async () => {
|
|
81
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
82
|
+
ok: false,
|
|
83
|
+
statusText: 'Internal Server Error',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const request: GraphRequest = {
|
|
87
|
+
viewMode: 'agent',
|
|
88
|
+
filters: {
|
|
89
|
+
correlation_id: null,
|
|
90
|
+
time_range: { preset: 'last10min' },
|
|
91
|
+
artifactTypes: [],
|
|
92
|
+
producers: [],
|
|
93
|
+
tags: [],
|
|
94
|
+
visibility: [],
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await expect(fetchGraphSnapshot(request)).rejects.toThrow('Graph API error: Internal Server Error');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle network errors gracefully', async () => {
|
|
102
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
103
|
+
|
|
104
|
+
const request: GraphRequest = {
|
|
105
|
+
viewMode: 'blackboard',
|
|
106
|
+
filters: {
|
|
107
|
+
correlation_id: null,
|
|
108
|
+
time_range: { preset: 'last5min' },
|
|
109
|
+
artifactTypes: [],
|
|
110
|
+
producers: [],
|
|
111
|
+
tags: [],
|
|
112
|
+
visibility: [],
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
await expect(fetchGraphSnapshot(request)).rejects.toThrow('Network error');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('mergeNodePositions - Priority Logic', () => {
|
|
121
|
+
it('should prioritize saved positions over all others', () => {
|
|
122
|
+
const backendNodes = [
|
|
123
|
+
{ id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 100, y: 100 }, hidden: false },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const savedPositions = new Map([['agent1', { x: 500, y: 500 }]]);
|
|
127
|
+
|
|
128
|
+
const currentNodes: Node[] = [
|
|
129
|
+
{ id: 'agent1', type: 'agent', data: { name: 'agent1' }, position: { x: 300, y: 300 } },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
|
|
133
|
+
|
|
134
|
+
// Saved position (500, 500) wins
|
|
135
|
+
expect(result[0]?.position).toEqual({ x: 500, y: 500 });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should prioritize current positions when no saved position exists', () => {
|
|
139
|
+
const backendNodes = [
|
|
140
|
+
{ id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 100, y: 100 }, hidden: false },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const savedPositions = new Map(); // No saved position
|
|
144
|
+
|
|
145
|
+
const currentNodes: Node[] = [
|
|
146
|
+
{ id: 'agent1', type: 'agent', data: { name: 'agent1' }, position: { x: 300, y: 300 } },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
|
|
150
|
+
|
|
151
|
+
// Current position (300, 300) wins
|
|
152
|
+
expect(result[0]!.position).toEqual({ x: 300, y: 300 });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should use backend position when saved and current are unavailable', () => {
|
|
156
|
+
const backendNodes = [
|
|
157
|
+
{ id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 100, y: 100 }, hidden: false },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const savedPositions = new Map(); // No saved position
|
|
161
|
+
const currentNodes: Node[] = []; // No current position
|
|
162
|
+
|
|
163
|
+
const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
|
|
164
|
+
|
|
165
|
+
// Backend position (100, 100) wins
|
|
166
|
+
expect(result[0]!.position).toEqual({ x: 100, y: 100 });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should generate random position when backend has zero coordinates', () => {
|
|
170
|
+
const backendNodes = [
|
|
171
|
+
{ id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 0, y: 0 }, hidden: false },
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const savedPositions = new Map(); // No saved position
|
|
175
|
+
const currentNodes: Node[] = []; // No current position
|
|
176
|
+
|
|
177
|
+
const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
|
|
178
|
+
|
|
179
|
+
// Random position should be generated (not 0, 0)
|
|
180
|
+
expect(result[0]!.position.x).toBeGreaterThan(0);
|
|
181
|
+
expect(result[0]!.position.y).toBeGreaterThan(0);
|
|
182
|
+
// Random position range: x in [400, 600], y in [300, 500]
|
|
183
|
+
expect(result[0]!.position.x).toBeGreaterThanOrEqual(400);
|
|
184
|
+
expect(result[0]!.position.x).toBeLessThanOrEqual(600);
|
|
185
|
+
expect(result[0]!.position.y).toBeGreaterThanOrEqual(300);
|
|
186
|
+
expect(result[0]!.position.y).toBeLessThanOrEqual(500);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle multiple nodes with mixed position sources', () => {
|
|
190
|
+
const backendNodes = [
|
|
191
|
+
{ id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 100, y: 100 }, hidden: false },
|
|
192
|
+
{ id: 'agent2', type: 'agent' as const, data: { name: 'agent2' }, position: { x: 200, y: 200 }, hidden: false },
|
|
193
|
+
{ id: 'agent3', type: 'agent' as const, data: { name: 'agent3' }, position: { x: 0, y: 0 }, hidden: false },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const savedPositions = new Map([['agent1', { x: 500, y: 500 }]]); // Only agent1
|
|
197
|
+
|
|
198
|
+
const currentNodes: Node[] = [
|
|
199
|
+
{ id: 'agent2', type: 'agent', data: { name: 'agent2' }, position: { x: 300, y: 300 } },
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
|
|
203
|
+
|
|
204
|
+
// agent1: saved position wins
|
|
205
|
+
expect(result[0]!.position).toEqual({ x: 500, y: 500 });
|
|
206
|
+
// agent2: current position wins
|
|
207
|
+
expect(result[1]!.position).toEqual({ x: 300, y: 300 });
|
|
208
|
+
// agent3: random position (backend is 0,0)
|
|
209
|
+
expect(result[2]!.position.x).toBeGreaterThan(0);
|
|
210
|
+
expect(result[2]!.position.y).toBeGreaterThan(0);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('overlayWebSocketState', () => {
|
|
215
|
+
it('should overlay agent status from WebSocket state', () => {
|
|
216
|
+
const nodes: Node[] = [
|
|
217
|
+
{
|
|
218
|
+
id: 'agent1',
|
|
219
|
+
type: 'agent',
|
|
220
|
+
data: { name: 'agent1', status: 'idle' },
|
|
221
|
+
position: { x: 100, y: 100 },
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const agentStatus = new Map([['agent1', 'running']]);
|
|
226
|
+
const streamingTokens = new Map<string, string[]>();
|
|
227
|
+
|
|
228
|
+
const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
|
|
229
|
+
|
|
230
|
+
expect(result[0]!.data.status).toBe('running');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should overlay streaming tokens from WebSocket state', () => {
|
|
234
|
+
const nodes: Node[] = [
|
|
235
|
+
{
|
|
236
|
+
id: 'agent1',
|
|
237
|
+
type: 'agent',
|
|
238
|
+
data: { name: 'agent1', status: 'idle' },
|
|
239
|
+
position: { x: 100, y: 100 },
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const agentStatus = new Map<string, string>();
|
|
244
|
+
const streamingTokens = new Map([['agent1', ['token1', 'token2', 'token3']]]);
|
|
245
|
+
|
|
246
|
+
const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
|
|
247
|
+
|
|
248
|
+
expect(result[0]!.data.streamingTokens).toEqual(['token1', 'token2', 'token3']);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should use backend status when WebSocket state is unavailable', () => {
|
|
252
|
+
const nodes: Node[] = [
|
|
253
|
+
{
|
|
254
|
+
id: 'agent1',
|
|
255
|
+
type: 'agent',
|
|
256
|
+
data: { name: 'agent1', status: 'idle' },
|
|
257
|
+
position: { x: 100, y: 100 },
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
const agentStatus = new Map<string, string>(); // Empty
|
|
262
|
+
const streamingTokens = new Map<string, string[]>(); // Empty
|
|
263
|
+
|
|
264
|
+
const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
|
|
265
|
+
|
|
266
|
+
// Backend status is preserved
|
|
267
|
+
expect(result[0]!.data.status).toBe('idle');
|
|
268
|
+
// Empty array for tokens
|
|
269
|
+
expect(result[0]!.data.streamingTokens).toEqual([]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should not modify non-agent nodes', () => {
|
|
273
|
+
const nodes: Node[] = [
|
|
274
|
+
{
|
|
275
|
+
id: 'message1',
|
|
276
|
+
type: 'message',
|
|
277
|
+
data: { type: 'Pizza', payload: {} },
|
|
278
|
+
position: { x: 100, y: 100 },
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const agentStatus = new Map([['message1', 'running']]);
|
|
283
|
+
const streamingTokens = new Map([['message1', ['token1']]]);
|
|
284
|
+
|
|
285
|
+
const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
|
|
286
|
+
|
|
287
|
+
// Message node should not be modified
|
|
288
|
+
expect(result[0]!.data).toEqual({ type: 'Pizza', payload: {} });
|
|
289
|
+
expect(result[0]!.data.status).toBeUndefined();
|
|
290
|
+
expect(result[0]!.data.streamingTokens).toBeUndefined();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should handle multiple agent nodes with mixed WebSocket state', () => {
|
|
294
|
+
const nodes: Node[] = [
|
|
295
|
+
{
|
|
296
|
+
id: 'agent1',
|
|
297
|
+
type: 'agent',
|
|
298
|
+
data: { name: 'agent1', status: 'idle' },
|
|
299
|
+
position: { x: 100, y: 100 },
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: 'agent2',
|
|
303
|
+
type: 'agent',
|
|
304
|
+
data: { name: 'agent2', status: 'idle' },
|
|
305
|
+
position: { x: 200, y: 200 },
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'message1',
|
|
309
|
+
type: 'message',
|
|
310
|
+
data: { type: 'Pizza' },
|
|
311
|
+
position: { x: 300, y: 300 },
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
const agentStatus = new Map([['agent1', 'running']]); // Only agent1
|
|
316
|
+
const streamingTokens = new Map([['agent2', ['token1', 'token2']]]); // Only agent2
|
|
317
|
+
|
|
318
|
+
const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
|
|
319
|
+
|
|
320
|
+
// agent1: status updated, no tokens
|
|
321
|
+
expect(result[0]!.data.status).toBe('running');
|
|
322
|
+
expect(result[0]!.data.streamingTokens).toEqual([]);
|
|
323
|
+
// agent2: status from backend, tokens updated
|
|
324
|
+
expect(result[1]!.data.status).toBe('idle');
|
|
325
|
+
expect(result[1]!.data.streamingTokens).toEqual(['token1', 'token2']);
|
|
326
|
+
// message1: unchanged
|
|
327
|
+
expect(result[2]!.data).toEqual({ type: 'Pizza' });
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { GraphRequest, GraphSnapshot, GraphNode } from '../types/graph';
|
|
2
|
+
import { Node } from '@xyflow/react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetch graph snapshot from backend
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchGraphSnapshot(
|
|
8
|
+
request: GraphRequest
|
|
9
|
+
): Promise<GraphSnapshot> {
|
|
10
|
+
const response = await fetch('/api/dashboard/graph', {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13
|
+
body: JSON.stringify(request),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Graph API error: ${response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return response.json();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Merge backend node positions with saved/current positions
|
|
25
|
+
* Priority: saved > current > backend > random
|
|
26
|
+
*/
|
|
27
|
+
export function mergeNodePositions(
|
|
28
|
+
backendNodes: GraphNode[],
|
|
29
|
+
savedPositions: Map<string, { x: number; y: number }>,
|
|
30
|
+
currentNodes: Node[]
|
|
31
|
+
): Node[] {
|
|
32
|
+
const currentPositions = new Map(
|
|
33
|
+
currentNodes.map(n => [n.id, n.position])
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return backendNodes.map(node => {
|
|
37
|
+
const position =
|
|
38
|
+
savedPositions.get(node.id) ||
|
|
39
|
+
currentPositions.get(node.id) ||
|
|
40
|
+
(node.position.x !== 0 || node.position.y !== 0 ? node.position : null) ||
|
|
41
|
+
randomPosition();
|
|
42
|
+
|
|
43
|
+
return { ...node, position };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function randomPosition() {
|
|
48
|
+
return {
|
|
49
|
+
x: 400 + Math.random() * 200,
|
|
50
|
+
y: 300 + Math.random() * 200,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Overlay real-time WebSocket state on backend nodes
|
|
56
|
+
*/
|
|
57
|
+
export function overlayWebSocketState(
|
|
58
|
+
nodes: Node[],
|
|
59
|
+
agentStatus: Map<string, string>,
|
|
60
|
+
streamingTokens: Map<string, string[]>
|
|
61
|
+
): Node[] {
|
|
62
|
+
return nodes.map(node => {
|
|
63
|
+
if (node.type === 'agent') {
|
|
64
|
+
return {
|
|
65
|
+
...node,
|
|
66
|
+
data: {
|
|
67
|
+
...node.data,
|
|
68
|
+
status: agentStatus.get(node.id) || node.data.status,
|
|
69
|
+
streamingTokens: streamingTokens.get(node.id) || [],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return node;
|
|
74
|
+
});
|
|
75
|
+
}
|