flock-core 0.5.2__py3-none-any.whl → 0.5.4__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 +16 -3
- flock/artifact_collector.py +158 -0
- flock/batch_accumulator.py +252 -0
- flock/correlation_engine.py +223 -0
- flock/dashboard/collector.py +4 -0
- flock/dashboard/events.py +74 -0
- flock/dashboard/graph_builder.py +272 -0
- flock/dashboard/models/graph.py +3 -1
- flock/dashboard/service.py +363 -14
- flock/frontend/package.json +1 -1
- flock/frontend/src/components/controls/PublishControl.test.tsx +11 -11
- flock/frontend/src/components/controls/PublishControl.tsx +1 -1
- flock/frontend/src/components/graph/AgentNode.tsx +4 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +4 -0
- flock/frontend/src/components/graph/LogicOperationsDisplay.tsx +463 -0
- flock/frontend/src/components/graph/PendingBatchEdge.tsx +141 -0
- flock/frontend/src/components/graph/PendingJoinEdge.tsx +144 -0
- flock/frontend/src/services/graphService.ts +3 -1
- flock/frontend/src/services/websocket.ts +99 -1
- flock/frontend/src/store/graphStore.test.ts +2 -1
- flock/frontend/src/store/graphStore.ts +36 -5
- flock/frontend/src/types/graph.ts +86 -0
- flock/orchestrator.py +263 -3
- flock/patches/__init__.py +1 -0
- flock/patches/dspy_streaming_patch.py +1 -0
- flock/subscription.py +70 -7
- {flock_core-0.5.2.dist-info → flock_core-0.5.4.dist-info}/METADATA +70 -14
- {flock_core-0.5.2.dist-info → flock_core-0.5.4.dist-info}/RECORD +31 -25
- {flock_core-0.5.2.dist-info → flock_core-0.5.4.dist-info}/WHEEL +0 -0
- {flock_core-0.5.2.dist-info → flock_core-0.5.4.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.2.dist-info → flock_core-0.5.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { memo, useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { AgentLogicOperations, CorrelationGroupState } from '../../types/graph';
|
|
3
|
+
|
|
4
|
+
interface LogicOperationsDisplayProps {
|
|
5
|
+
logicOperations: AgentLogicOperations[];
|
|
6
|
+
compactNodeView?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Phase 1.4: Logic Operations UX - Visual display component
|
|
11
|
+
*
|
|
12
|
+
* Displays JoinSpec and BatchSpec waiting states in agent nodes:
|
|
13
|
+
* - JoinSpec: Shows correlation groups, waiting_for types, expiration timers
|
|
14
|
+
* - BatchSpec: Shows items collected, target size, timeout remaining
|
|
15
|
+
*
|
|
16
|
+
* Enhanced with:
|
|
17
|
+
* - Client-side timer countdown for real-time updates
|
|
18
|
+
* - Truncation for long correlation keys
|
|
19
|
+
* - Batch progress bars
|
|
20
|
+
* - Timeout warning animations
|
|
21
|
+
* - Copy-to-clipboard for correlation keys
|
|
22
|
+
* - Flash animations on updates
|
|
23
|
+
* - Accessibility attributes
|
|
24
|
+
*/
|
|
25
|
+
const LogicOperationsDisplay = memo(({ logicOperations, compactNodeView = false }: LogicOperationsDisplayProps) => {
|
|
26
|
+
const [clientTime, setClientTime] = useState(Date.now());
|
|
27
|
+
const [flashingGroups, setFlashingGroups] = useState<Set<string>>(new Set());
|
|
28
|
+
const prevGroupsRef = useRef<Map<string, CorrelationGroupState>>(new Map());
|
|
29
|
+
|
|
30
|
+
// Client-side timer: Update every second for real-time countdown
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const interval = setInterval(() => {
|
|
33
|
+
setClientTime(Date.now());
|
|
34
|
+
}, 1000);
|
|
35
|
+
|
|
36
|
+
return () => clearInterval(interval);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
// Detect new/updated correlation groups for flash animation
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!logicOperations) return;
|
|
42
|
+
|
|
43
|
+
const newFlashing = new Set<string>();
|
|
44
|
+
|
|
45
|
+
logicOperations.forEach(op => {
|
|
46
|
+
if (op.waiting_state?.correlation_groups) {
|
|
47
|
+
op.waiting_state.correlation_groups.forEach(group => {
|
|
48
|
+
const key = `${op.subscription_index}-${group.correlation_key}`;
|
|
49
|
+
const prev = prevGroupsRef.current.get(key);
|
|
50
|
+
|
|
51
|
+
if (!prev || JSON.stringify(prev.collected_types) !== JSON.stringify(group.collected_types)) {
|
|
52
|
+
newFlashing.add(key);
|
|
53
|
+
prevGroupsRef.current.set(key, group);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (newFlashing.size > 0) {
|
|
60
|
+
setFlashingGroups(newFlashing);
|
|
61
|
+
const timer = setTimeout(() => setFlashingGroups(new Set()), 500);
|
|
62
|
+
return () => clearTimeout(timer);
|
|
63
|
+
}
|
|
64
|
+
}, [logicOperations]);
|
|
65
|
+
|
|
66
|
+
if (!logicOperations || logicOperations.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Only show logic operations if agent is waiting
|
|
71
|
+
const waitingOperations = logicOperations.filter(op => op.waiting_state?.is_waiting);
|
|
72
|
+
|
|
73
|
+
if (waitingOperations.length === 0) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Helper: Truncate long correlation keys
|
|
78
|
+
const truncateKey = (key: string, maxLength: number = 20): string => {
|
|
79
|
+
if (!key || key.length <= maxLength) return key;
|
|
80
|
+
return `${key.substring(0, maxLength - 3)}...`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Helper: Copy to clipboard
|
|
84
|
+
const copyToClipboard = (text: string) => {
|
|
85
|
+
navigator.clipboard.writeText(text).catch(() => {
|
|
86
|
+
// Silently fail if clipboard not available
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Helper: Calculate client-side elapsed/remaining time
|
|
91
|
+
const calculateClientTime = (createdAt: string, serverElapsed: number, serverRemaining: number | null) => {
|
|
92
|
+
if (!createdAt) return { elapsed: serverElapsed, remaining: serverRemaining };
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const created = new Date(createdAt).getTime();
|
|
96
|
+
const now = clientTime;
|
|
97
|
+
const clientElapsed = (now - created) / 1000; // seconds
|
|
98
|
+
|
|
99
|
+
let clientRemaining = serverRemaining;
|
|
100
|
+
if (serverRemaining !== null && serverRemaining !== undefined) {
|
|
101
|
+
const totalWindow = serverElapsed + serverRemaining;
|
|
102
|
+
clientRemaining = Math.max(0, totalWindow - clientElapsed);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
elapsed: Math.max(0, clientElapsed),
|
|
107
|
+
remaining: clientRemaining,
|
|
108
|
+
};
|
|
109
|
+
} catch {
|
|
110
|
+
return { elapsed: serverElapsed, remaining: serverRemaining };
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '8px' }}>
|
|
116
|
+
{waitingOperations.map((operation, idx) => (
|
|
117
|
+
<div key={`logic-op-${idx}`}>
|
|
118
|
+
{/* JoinSpec Waiting State */}
|
|
119
|
+
{operation.join && operation.waiting_state?.correlation_groups && operation.waiting_state.correlation_groups.length > 0 && (
|
|
120
|
+
<div
|
|
121
|
+
style={{
|
|
122
|
+
padding: '8px 10px',
|
|
123
|
+
background: 'rgba(168, 85, 247, 0.08)',
|
|
124
|
+
borderLeft: '3px solid var(--color-purple-500, #a855f7)',
|
|
125
|
+
borderRadius: 'var(--radius-md)',
|
|
126
|
+
boxShadow: 'var(--shadow-xs)',
|
|
127
|
+
}}
|
|
128
|
+
role="region"
|
|
129
|
+
aria-label="JoinSpec correlation groups"
|
|
130
|
+
>
|
|
131
|
+
{operation.waiting_state.correlation_groups.map((group, groupIdx) => {
|
|
132
|
+
const groupKey = `${operation.subscription_index}-${group.correlation_key}`;
|
|
133
|
+
const isFlashing = flashingGroups.has(groupKey);
|
|
134
|
+
const { elapsed, remaining } = calculateClientTime(
|
|
135
|
+
group.created_at,
|
|
136
|
+
group.elapsed_seconds,
|
|
137
|
+
group.expires_in_seconds
|
|
138
|
+
);
|
|
139
|
+
const isUrgent = remaining !== null && remaining < 30;
|
|
140
|
+
const isCritical = remaining !== null && remaining < 10;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
key={`group-${groupIdx}`}
|
|
145
|
+
style={{
|
|
146
|
+
marginBottom: groupIdx < operation.waiting_state!.correlation_groups!.length - 1 ? '8px' : '0',
|
|
147
|
+
background: isFlashing ? 'rgba(168, 85, 247, 0.15)' : 'transparent',
|
|
148
|
+
padding: isFlashing ? '4px' : '0',
|
|
149
|
+
borderRadius: 'var(--radius-sm)',
|
|
150
|
+
transition: 'all 0.3s ease',
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
{/* Header: JoinSpec icon + correlation key */}
|
|
154
|
+
<div style={{
|
|
155
|
+
display: 'flex',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
gap: '6px',
|
|
158
|
+
marginBottom: '6px',
|
|
159
|
+
}}>
|
|
160
|
+
<div
|
|
161
|
+
style={{
|
|
162
|
+
display: 'flex',
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
justifyContent: 'center',
|
|
165
|
+
width: '20px',
|
|
166
|
+
height: '20px',
|
|
167
|
+
borderRadius: 'var(--radius-sm)',
|
|
168
|
+
background: 'var(--color-purple-100, #f3e8ff)',
|
|
169
|
+
color: 'var(--color-purple-700, #7e22ce)',
|
|
170
|
+
fontSize: '12px',
|
|
171
|
+
fontWeight: 700,
|
|
172
|
+
}}
|
|
173
|
+
aria-label="Correlation join operation"
|
|
174
|
+
>
|
|
175
|
+
⋈
|
|
176
|
+
</div>
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
fontSize: '10px',
|
|
180
|
+
fontFamily: 'var(--font-family-mono)',
|
|
181
|
+
color: 'var(--color-purple-700, #7e22ce)',
|
|
182
|
+
fontWeight: 600,
|
|
183
|
+
cursor: 'pointer',
|
|
184
|
+
userSelect: 'none',
|
|
185
|
+
}}
|
|
186
|
+
title={`${group.correlation_key}\nClick to copy`}
|
|
187
|
+
onClick={() => copyToClipboard(group.correlation_key)}
|
|
188
|
+
>
|
|
189
|
+
{truncateKey(group.correlation_key)}
|
|
190
|
+
</div>
|
|
191
|
+
{group.correlation_key.length > 20 && (
|
|
192
|
+
<button
|
|
193
|
+
onClick={() => copyToClipboard(group.correlation_key)}
|
|
194
|
+
style={{
|
|
195
|
+
background: 'none',
|
|
196
|
+
border: 'none',
|
|
197
|
+
cursor: 'pointer',
|
|
198
|
+
fontSize: '10px',
|
|
199
|
+
padding: '0',
|
|
200
|
+
color: 'var(--color-purple-600, #9333ea)',
|
|
201
|
+
opacity: 0.7,
|
|
202
|
+
}}
|
|
203
|
+
title="Copy full correlation key"
|
|
204
|
+
aria-label="Copy correlation key to clipboard"
|
|
205
|
+
>
|
|
206
|
+
📋
|
|
207
|
+
</button>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* Waiting for types */}
|
|
212
|
+
{!compactNodeView && group.waiting_for && group.waiting_for.length > 0 && (
|
|
213
|
+
<div style={{ marginBottom: '4px' }}>
|
|
214
|
+
<div style={{
|
|
215
|
+
fontSize: '9px',
|
|
216
|
+
color: 'var(--color-text-tertiary)',
|
|
217
|
+
textTransform: 'uppercase',
|
|
218
|
+
letterSpacing: '0.5px',
|
|
219
|
+
fontWeight: 600,
|
|
220
|
+
marginBottom: '3px',
|
|
221
|
+
}}>
|
|
222
|
+
Waiting for:
|
|
223
|
+
</div>
|
|
224
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
|
|
225
|
+
{group.waiting_for.map((type, typeIdx) => (
|
|
226
|
+
<div
|
|
227
|
+
key={`waiting-${typeIdx}`}
|
|
228
|
+
style={{
|
|
229
|
+
padding: '2px 6px',
|
|
230
|
+
background: 'var(--color-purple-100, #f3e8ff)',
|
|
231
|
+
color: 'var(--color-purple-700, #7e22ce)',
|
|
232
|
+
borderRadius: 'var(--radius-sm)',
|
|
233
|
+
fontSize: '9px',
|
|
234
|
+
fontFamily: 'var(--font-family-mono)',
|
|
235
|
+
fontWeight: 600,
|
|
236
|
+
}}
|
|
237
|
+
title={`Missing artifact type: ${type}`}
|
|
238
|
+
>
|
|
239
|
+
{type}
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{/* Progress & Expiration */}
|
|
247
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '10px' }}>
|
|
248
|
+
{/* Collected types indicator */}
|
|
249
|
+
{group.collected_types && Object.keys(group.collected_types).length > 0 && (
|
|
250
|
+
<div
|
|
251
|
+
style={{
|
|
252
|
+
display: 'flex',
|
|
253
|
+
alignItems: 'center',
|
|
254
|
+
gap: '4px',
|
|
255
|
+
color: 'var(--color-purple-600, #9333ea)',
|
|
256
|
+
}}
|
|
257
|
+
title={`Collected ${Object.keys(group.collected_types).length} out of ${Object.keys(group.required_types || {}).length} required types`}
|
|
258
|
+
>
|
|
259
|
+
<span style={{ fontWeight: 600 }}>{Object.keys(group.collected_types).length}</span>
|
|
260
|
+
{group.required_types && (
|
|
261
|
+
<span style={{ fontSize: '9px', opacity: 0.8 }}>/{Object.keys(group.required_types).length} types</span>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{/* Expiration timer with client-side countdown */}
|
|
267
|
+
{remaining !== null && remaining !== undefined && (
|
|
268
|
+
<div
|
|
269
|
+
style={{
|
|
270
|
+
display: 'flex',
|
|
271
|
+
alignItems: 'center',
|
|
272
|
+
gap: '4px',
|
|
273
|
+
color: isCritical
|
|
274
|
+
? 'var(--color-error, #ef4444)'
|
|
275
|
+
: isUrgent
|
|
276
|
+
? 'var(--color-warning-light)'
|
|
277
|
+
: 'var(--color-text-secondary)',
|
|
278
|
+
fontWeight: isUrgent ? 600 : 400,
|
|
279
|
+
animation: isCritical ? 'pulse 1s infinite' : 'none',
|
|
280
|
+
}}
|
|
281
|
+
title={`Expires in ${Math.round(remaining)} seconds`}
|
|
282
|
+
aria-live="polite"
|
|
283
|
+
aria-atomic="true"
|
|
284
|
+
>
|
|
285
|
+
<span>⏱</span>
|
|
286
|
+
<span>{Math.round(remaining)}s</span>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{/* Elapsed time */}
|
|
291
|
+
{!compactNodeView && (
|
|
292
|
+
<div
|
|
293
|
+
style={{
|
|
294
|
+
fontSize: '9px',
|
|
295
|
+
color: 'var(--color-text-tertiary)',
|
|
296
|
+
opacity: 0.7,
|
|
297
|
+
}}
|
|
298
|
+
title={`Elapsed time: ${Math.round(elapsed)} seconds`}
|
|
299
|
+
>
|
|
300
|
+
{Math.round(elapsed)}s elapsed
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
})}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{/* BatchSpec Waiting State */}
|
|
311
|
+
{operation.batch && operation.waiting_state?.batch_state && (
|
|
312
|
+
<div
|
|
313
|
+
style={{
|
|
314
|
+
padding: '8px 10px',
|
|
315
|
+
background: 'rgba(251, 146, 60, 0.08)',
|
|
316
|
+
borderLeft: '3px solid var(--color-orange-500, #fb923c)',
|
|
317
|
+
borderRadius: 'var(--radius-md)',
|
|
318
|
+
boxShadow: 'var(--shadow-xs)',
|
|
319
|
+
}}
|
|
320
|
+
role="region"
|
|
321
|
+
aria-label="BatchSpec accumulation"
|
|
322
|
+
>
|
|
323
|
+
{(() => {
|
|
324
|
+
const batchState = operation.waiting_state.batch_state;
|
|
325
|
+
const { remaining } = calculateClientTime(
|
|
326
|
+
batchState.created_at,
|
|
327
|
+
batchState.elapsed_seconds,
|
|
328
|
+
batchState.timeout_remaining_seconds || null
|
|
329
|
+
);
|
|
330
|
+
const isTimeoutUrgent = remaining !== null && remaining < 10;
|
|
331
|
+
const progressPercent = batchState.items_target
|
|
332
|
+
? (batchState.items_collected / batchState.items_target) * 100
|
|
333
|
+
: 0;
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<>
|
|
337
|
+
{/* Header: BatchSpec icon */}
|
|
338
|
+
<div style={{
|
|
339
|
+
display: 'flex',
|
|
340
|
+
alignItems: 'center',
|
|
341
|
+
gap: '6px',
|
|
342
|
+
marginBottom: '6px',
|
|
343
|
+
}}>
|
|
344
|
+
<div
|
|
345
|
+
style={{
|
|
346
|
+
display: 'flex',
|
|
347
|
+
alignItems: 'center',
|
|
348
|
+
justifyContent: 'center',
|
|
349
|
+
width: '20px',
|
|
350
|
+
height: '20px',
|
|
351
|
+
borderRadius: 'var(--radius-sm)',
|
|
352
|
+
background: 'var(--color-orange-100, #ffedd5)',
|
|
353
|
+
color: 'var(--color-orange-700, #c2410c)',
|
|
354
|
+
fontSize: '12px',
|
|
355
|
+
fontWeight: 700,
|
|
356
|
+
}}
|
|
357
|
+
aria-label="Batch accumulation operation"
|
|
358
|
+
>
|
|
359
|
+
⊞
|
|
360
|
+
</div>
|
|
361
|
+
<div style={{
|
|
362
|
+
fontSize: '10px',
|
|
363
|
+
fontFamily: 'var(--font-family-mono)',
|
|
364
|
+
color: 'var(--color-orange-700, #c2410c)',
|
|
365
|
+
fontWeight: 600,
|
|
366
|
+
}}>
|
|
367
|
+
Batch Accumulating
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
{/* Batch progress */}
|
|
372
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
373
|
+
{/* Items collected */}
|
|
374
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '10px' }}>
|
|
375
|
+
<div
|
|
376
|
+
style={{
|
|
377
|
+
display: 'flex',
|
|
378
|
+
alignItems: 'center',
|
|
379
|
+
gap: '4px',
|
|
380
|
+
color: 'var(--color-orange-600, #ea580c)',
|
|
381
|
+
}}
|
|
382
|
+
title={`Collected ${batchState.items_collected} items${batchState.items_target ? ` out of ${batchState.items_target}` : ''}`}
|
|
383
|
+
>
|
|
384
|
+
<span style={{ fontWeight: 600 }}>{batchState.items_collected}</span>
|
|
385
|
+
{batchState.items_target !== null && (
|
|
386
|
+
<>
|
|
387
|
+
<span style={{ fontSize: '9px', opacity: 0.8 }}>/{batchState.items_target} items</span>
|
|
388
|
+
</>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
{/* Timeout remaining with client-side countdown */}
|
|
393
|
+
{remaining !== null && remaining !== undefined && (
|
|
394
|
+
<div
|
|
395
|
+
style={{
|
|
396
|
+
display: 'flex',
|
|
397
|
+
alignItems: 'center',
|
|
398
|
+
gap: '4px',
|
|
399
|
+
color: isTimeoutUrgent ? 'var(--color-error, #ef4444)' : 'var(--color-text-secondary)',
|
|
400
|
+
fontWeight: isTimeoutUrgent ? 600 : 400,
|
|
401
|
+
animation: isTimeoutUrgent ? 'pulse 1s infinite' : 'none',
|
|
402
|
+
}}
|
|
403
|
+
title={`Timeout in ${Math.round(remaining)} seconds`}
|
|
404
|
+
aria-live="polite"
|
|
405
|
+
aria-atomic="true"
|
|
406
|
+
>
|
|
407
|
+
<span>⏱</span>
|
|
408
|
+
<span>{Math.round(remaining)}s</span>
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
{/* Progress bar (only if target is set) */}
|
|
414
|
+
{batchState.items_target !== null && (
|
|
415
|
+
<div
|
|
416
|
+
style={{
|
|
417
|
+
width: '100%',
|
|
418
|
+
height: '4px',
|
|
419
|
+
background: 'var(--color-orange-100, #ffedd5)',
|
|
420
|
+
borderRadius: '2px',
|
|
421
|
+
overflow: 'hidden',
|
|
422
|
+
marginTop: '2px'
|
|
423
|
+
}}
|
|
424
|
+
title={`Progress: ${Math.round(progressPercent)}%`}
|
|
425
|
+
role="progressbar"
|
|
426
|
+
aria-valuenow={batchState.items_collected}
|
|
427
|
+
aria-valuemin={0}
|
|
428
|
+
aria-valuemax={batchState.items_target}
|
|
429
|
+
>
|
|
430
|
+
<div style={{
|
|
431
|
+
width: `${Math.min(100, progressPercent)}%`,
|
|
432
|
+
height: '100%',
|
|
433
|
+
background: 'var(--color-orange-500, #fb923c)',
|
|
434
|
+
transition: 'width 0.3s ease',
|
|
435
|
+
}} />
|
|
436
|
+
</div>
|
|
437
|
+
)}
|
|
438
|
+
|
|
439
|
+
{/* Flush trigger indicator */}
|
|
440
|
+
{!compactNodeView && batchState.will_flush && (
|
|
441
|
+
<div style={{
|
|
442
|
+
fontSize: '9px',
|
|
443
|
+
color: 'var(--color-text-tertiary)',
|
|
444
|
+
fontStyle: 'italic',
|
|
445
|
+
}}>
|
|
446
|
+
Will flush: {batchState.will_flush === 'on_size' ? 'on size' : batchState.will_flush === 'on_timeout' ? 'on timeout' : 'unknown'}
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
</>
|
|
451
|
+
);
|
|
452
|
+
})()}
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
))}
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
LogicOperationsDisplay.displayName = 'LogicOperationsDisplay';
|
|
462
|
+
|
|
463
|
+
export default LogicOperationsDisplay;
|
|
@@ -0,0 +1,141 @@
|
|
|
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 1.5: Logic Operations UX - PendingBatchEdge Component
|
|
15
|
+
*
|
|
16
|
+
* Custom edge for showing artifacts "en route" to BatchSpec accumulation.
|
|
17
|
+
* Visually distinct from normal message_flow edges to indicate batching state.
|
|
18
|
+
*
|
|
19
|
+
* Features:
|
|
20
|
+
* - Orange dashed line (matches BatchSpec theme)
|
|
21
|
+
* - Label with ⊞ symbol + batch progress
|
|
22
|
+
* - Hover tooltip showing batch size/target
|
|
23
|
+
* - Animated dashing to show "accumulating" state
|
|
24
|
+
*
|
|
25
|
+
* Edge type: "pending_batch"
|
|
26
|
+
* Created by backend graph_builder.py when artifacts are accumulating in batches
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export interface PendingBatchEdgeData {
|
|
30
|
+
artifactId: string;
|
|
31
|
+
artifactType: string;
|
|
32
|
+
subscriptionIndex: number;
|
|
33
|
+
itemsCollected: number;
|
|
34
|
+
itemsTarget: number | null;
|
|
35
|
+
labelOffset?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PendingBatchEdge: React.FC<EdgeProps> = ({
|
|
39
|
+
id,
|
|
40
|
+
sourceX,
|
|
41
|
+
sourceY,
|
|
42
|
+
targetX,
|
|
43
|
+
targetY,
|
|
44
|
+
sourcePosition,
|
|
45
|
+
targetPosition,
|
|
46
|
+
label,
|
|
47
|
+
style = {},
|
|
48
|
+
markerEnd,
|
|
49
|
+
data,
|
|
50
|
+
}) => {
|
|
51
|
+
// Get edge settings from settings store
|
|
52
|
+
const edgeType = useSettingsStore((state) => state.graph.edgeType);
|
|
53
|
+
const edgeStrokeWidth = useSettingsStore((state) => state.graph.edgeStrokeWidth);
|
|
54
|
+
const showEdgeLabels = useSettingsStore((state) => state.graph.showEdgeLabels);
|
|
55
|
+
|
|
56
|
+
// Use appropriate path function based on settings
|
|
57
|
+
const getPath = () => {
|
|
58
|
+
const params = { sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition };
|
|
59
|
+
switch (edgeType) {
|
|
60
|
+
case 'straight':
|
|
61
|
+
return getStraightPath(params);
|
|
62
|
+
case 'smoothstep':
|
|
63
|
+
return getSmoothStepPath(params);
|
|
64
|
+
case 'simplebezier':
|
|
65
|
+
return getSimpleBezierPath(params);
|
|
66
|
+
case 'bezier':
|
|
67
|
+
default:
|
|
68
|
+
return getBezierPath(params);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const [edgePath, labelX, labelY] = getPath();
|
|
73
|
+
|
|
74
|
+
const edgeData = data as PendingBatchEdgeData | undefined;
|
|
75
|
+
const labelOffset = edgeData?.labelOffset || 0;
|
|
76
|
+
const itemsCollected = edgeData?.itemsCollected || 0;
|
|
77
|
+
const itemsTarget = edgeData?.itemsTarget;
|
|
78
|
+
|
|
79
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
80
|
+
|
|
81
|
+
// Orange color theme for BatchSpec
|
|
82
|
+
const edgeColor = 'var(--color-orange-500, #fb923c)';
|
|
83
|
+
const edgeColorHover = 'var(--color-orange-600, #ea580c)';
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<BaseEdge
|
|
88
|
+
id={id}
|
|
89
|
+
path={edgePath}
|
|
90
|
+
style={{
|
|
91
|
+
...style,
|
|
92
|
+
stroke: isHovered ? edgeColorHover : edgeColor,
|
|
93
|
+
strokeWidth: isHovered ? edgeStrokeWidth + 1 : edgeStrokeWidth,
|
|
94
|
+
strokeDasharray: '8,6', // Dashed line for "pending" state
|
|
95
|
+
opacity: 0.7,
|
|
96
|
+
transition: 'var(--transition-all)',
|
|
97
|
+
filter: isHovered ? `drop-shadow(0 0 6px ${edgeColor})` : 'none',
|
|
98
|
+
animation: 'pending-dash 2s linear infinite',
|
|
99
|
+
}}
|
|
100
|
+
markerEnd={markerEnd}
|
|
101
|
+
/>
|
|
102
|
+
{label && showEdgeLabels && (
|
|
103
|
+
<EdgeLabelRenderer>
|
|
104
|
+
<div
|
|
105
|
+
style={{
|
|
106
|
+
position: 'absolute',
|
|
107
|
+
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY + labelOffset}px)`,
|
|
108
|
+
fontSize: 'var(--font-size-caption)',
|
|
109
|
+
fontWeight: 'var(--font-weight-semibold)',
|
|
110
|
+
background: 'rgba(251, 146, 60, 0.12)', // Orange tinted background
|
|
111
|
+
color: 'var(--color-orange-700, #c2410c)',
|
|
112
|
+
padding: '4px 8px',
|
|
113
|
+
borderRadius: 'var(--radius-sm)',
|
|
114
|
+
border: `1.5px dashed ${edgeColor}`,
|
|
115
|
+
pointerEvents: 'all',
|
|
116
|
+
backdropFilter: 'blur(var(--blur-sm))',
|
|
117
|
+
boxShadow: isHovered ? 'var(--shadow-md)' : 'var(--shadow-xs)',
|
|
118
|
+
transition: 'var(--transition-all)',
|
|
119
|
+
cursor: 'help',
|
|
120
|
+
}}
|
|
121
|
+
className="nodrag nopan"
|
|
122
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
123
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
124
|
+
title={`Batch accumulating: ${itemsCollected}${itemsTarget ? `/${itemsTarget}` : ''} items\nArtifact: ${edgeData?.artifactType || 'unknown'}`}
|
|
125
|
+
>
|
|
126
|
+
{label}
|
|
127
|
+
</div>
|
|
128
|
+
</EdgeLabelRenderer>
|
|
129
|
+
)}
|
|
130
|
+
<style>{`
|
|
131
|
+
@keyframes pending-dash {
|
|
132
|
+
to {
|
|
133
|
+
stroke-dashoffset: -28;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
`}</style>
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default PendingBatchEdge;
|