flock-core 0.5.3__py3-none-any.whl → 0.5.5__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.

Files changed (44) hide show
  1. flock/agent.py +20 -1
  2. flock/artifact_collector.py +2 -3
  3. flock/batch_accumulator.py +4 -4
  4. flock/components.py +32 -0
  5. flock/correlation_engine.py +9 -4
  6. flock/dashboard/collector.py +4 -0
  7. flock/dashboard/events.py +74 -0
  8. flock/dashboard/graph_builder.py +272 -0
  9. flock/dashboard/models/graph.py +3 -1
  10. flock/dashboard/service.py +363 -14
  11. flock/dashboard/static_v2/assets/index-DFRnI_mt.js +1 -1
  12. flock/dashboard/static_v2/index.html +3 -3
  13. flock/engines/dspy_engine.py +41 -3
  14. flock/engines/examples/__init__.py +6 -0
  15. flock/engines/examples/simple_batch_engine.py +61 -0
  16. flock/frontend/README.md +4 -4
  17. flock/frontend/docs/DESIGN_SYSTEM.md +1 -1
  18. flock/frontend/package-lock.json +2 -2
  19. flock/frontend/package.json +2 -2
  20. flock/frontend/src/components/controls/PublishControl.test.tsx +11 -11
  21. flock/frontend/src/components/controls/PublishControl.tsx +1 -1
  22. flock/frontend/src/components/graph/AgentNode.tsx +4 -0
  23. flock/frontend/src/components/graph/GraphCanvas.tsx +4 -0
  24. flock/frontend/src/components/graph/LogicOperationsDisplay.tsx +463 -0
  25. flock/frontend/src/components/graph/PendingBatchEdge.tsx +141 -0
  26. flock/frontend/src/components/graph/PendingJoinEdge.tsx +144 -0
  27. flock/frontend/src/components/settings/SettingsPanel.css +1 -1
  28. flock/frontend/src/components/settings/ThemeSelector.tsx +2 -2
  29. flock/frontend/src/services/graphService.ts +3 -1
  30. flock/frontend/src/services/indexeddb.ts +1 -1
  31. flock/frontend/src/services/websocket.ts +99 -1
  32. flock/frontend/src/store/graphStore.test.ts +2 -1
  33. flock/frontend/src/store/graphStore.ts +36 -5
  34. flock/frontend/src/styles/variables.css +1 -1
  35. flock/frontend/src/types/graph.ts +86 -0
  36. flock/orchestrator.py +268 -13
  37. flock/patches/__init__.py +1 -0
  38. flock/patches/dspy_streaming_patch.py +1 -0
  39. flock/runtime.py +3 -0
  40. {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/METADATA +11 -1
  41. {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/RECORD +44 -39
  42. {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/WHEEL +0 -0
  43. {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/entry_points.txt +0 -0
  44. {flock_core-0.5.3.dist-info → flock_core-0.5.5.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;