flock-core 0.5.0b70__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 +28 -9
- 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.0b70.dist-info → flock_core-0.5.0b75.dist-info}/METADATA +1 -1
- {flock_core-0.5.0b70.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.0b70.dist-info → flock_core-0.5.0b75.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b70.dist-info → flock_core-0.5.0b75.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b70.dist-info → flock_core-0.5.0b75.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { Rnd } from 'react-rnd';
|
|
3
|
+
import { useUIStore } from '../../store/uiStore';
|
|
4
|
+
import JsonAttributeRenderer from '../modules/JsonAttributeRenderer';
|
|
5
|
+
import { fetchArtifacts, type ArtifactListItem } from '../../services/api';
|
|
6
|
+
|
|
7
|
+
interface MessageDetailWindowProps {
|
|
8
|
+
nodeId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MessageDetailWindow: React.FC<MessageDetailWindowProps> = ({ nodeId }) => {
|
|
12
|
+
const window = useUIStore((state) => state.detailWindows.get(nodeId));
|
|
13
|
+
const updateDetailWindow = useUIStore((state) => state.updateDetailWindow);
|
|
14
|
+
const closeDetailWindow = useUIStore((state) => state.closeDetailWindow);
|
|
15
|
+
|
|
16
|
+
const [artifact, setArtifact] = useState<ArtifactListItem | null>(null);
|
|
17
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
18
|
+
const [error, setError] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
const handleClose = useCallback(() => {
|
|
21
|
+
closeDetailWindow(nodeId);
|
|
22
|
+
}, [nodeId, closeDetailWindow]);
|
|
23
|
+
|
|
24
|
+
// Fetch artifact details from backend
|
|
25
|
+
// Note: We query recent artifacts and filter client-side by ID
|
|
26
|
+
// because there's no single artifact endpoint yet
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const fetchArtifactDetails = async () => {
|
|
29
|
+
setIsLoading(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Fetch recent artifacts with consumption metadata
|
|
34
|
+
const response = await fetchArtifacts({
|
|
35
|
+
limit: 100,
|
|
36
|
+
embedMeta: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Find the artifact matching this node ID
|
|
40
|
+
const matchingArtifact = response.items.find((item) => item.id === nodeId);
|
|
41
|
+
|
|
42
|
+
if (!matchingArtifact) {
|
|
43
|
+
throw new Error('Artifact not found');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setArtifact(matchingArtifact);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('Failed to fetch artifact details:', err);
|
|
49
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
50
|
+
} finally {
|
|
51
|
+
setIsLoading(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
fetchArtifactDetails();
|
|
56
|
+
}, [nodeId]);
|
|
57
|
+
|
|
58
|
+
if (!window) return null;
|
|
59
|
+
|
|
60
|
+
const { position, size } = window;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Rnd
|
|
64
|
+
position={position}
|
|
65
|
+
size={size}
|
|
66
|
+
onDragStop={(_e, d) => {
|
|
67
|
+
updateDetailWindow(nodeId, {
|
|
68
|
+
position: { x: d.x, y: d.y },
|
|
69
|
+
});
|
|
70
|
+
}}
|
|
71
|
+
onResizeStop={(_e, _direction, ref, _delta, position) => {
|
|
72
|
+
updateDetailWindow(nodeId, {
|
|
73
|
+
size: {
|
|
74
|
+
width: parseInt(ref.style.width, 10),
|
|
75
|
+
height: parseInt(ref.style.height, 10),
|
|
76
|
+
},
|
|
77
|
+
position,
|
|
78
|
+
});
|
|
79
|
+
}}
|
|
80
|
+
minWidth={600}
|
|
81
|
+
minHeight={400}
|
|
82
|
+
bounds="parent"
|
|
83
|
+
dragHandleClassName="window-header"
|
|
84
|
+
style={{
|
|
85
|
+
zIndex: 1000,
|
|
86
|
+
display: 'flex',
|
|
87
|
+
flexDirection: 'column',
|
|
88
|
+
pointerEvents: 'all',
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
display: 'flex',
|
|
94
|
+
flexDirection: 'column',
|
|
95
|
+
width: '100%',
|
|
96
|
+
height: '100%',
|
|
97
|
+
background: 'var(--color-glass-bg)',
|
|
98
|
+
border: 'var(--border-width-1) solid var(--color-glass-border)',
|
|
99
|
+
borderRadius: 'var(--radius-xl)',
|
|
100
|
+
overflow: 'hidden',
|
|
101
|
+
boxShadow: 'var(--shadow-xl)',
|
|
102
|
+
backdropFilter: 'blur(var(--blur-lg))',
|
|
103
|
+
WebkitBackdropFilter: 'blur(var(--blur-lg))',
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{/* Header */}
|
|
107
|
+
<div
|
|
108
|
+
className="window-header"
|
|
109
|
+
style={{
|
|
110
|
+
display: 'flex',
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
justifyContent: 'space-between',
|
|
113
|
+
padding: 'var(--space-component-md) var(--space-component-lg)',
|
|
114
|
+
background: 'rgba(42, 42, 50, 0.5)',
|
|
115
|
+
borderBottom: 'var(--border-width-1) solid var(--color-border-subtle)',
|
|
116
|
+
cursor: 'move',
|
|
117
|
+
userSelect: 'none',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap-xs)' }}>
|
|
121
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--gap-md)' }}>
|
|
122
|
+
<div
|
|
123
|
+
style={{
|
|
124
|
+
width: '10px',
|
|
125
|
+
height: '10px',
|
|
126
|
+
borderRadius: 'var(--radius-circle)',
|
|
127
|
+
background: 'var(--color-warning)',
|
|
128
|
+
boxShadow: '0 0 8px var(--color-warning)',
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
<span
|
|
132
|
+
style={{
|
|
133
|
+
color: 'var(--color-text-primary)',
|
|
134
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
135
|
+
fontWeight: 'var(--font-weight-semibold)',
|
|
136
|
+
fontFamily: 'var(--font-family-sans)',
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
Message: {artifact?.type || nodeId}
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
<div
|
|
143
|
+
style={{
|
|
144
|
+
fontSize: 'var(--font-size-caption)',
|
|
145
|
+
color: 'var(--color-text-tertiary)',
|
|
146
|
+
fontFamily: 'var(--font-family-mono)',
|
|
147
|
+
paddingLeft: 'calc(10px + var(--gap-md))', // Align with message text
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
id: {nodeId}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<button
|
|
154
|
+
onClick={handleClose}
|
|
155
|
+
aria-label="Close window"
|
|
156
|
+
style={{
|
|
157
|
+
background: 'transparent',
|
|
158
|
+
border: 'none',
|
|
159
|
+
color: 'var(--color-text-secondary)',
|
|
160
|
+
fontSize: 'var(--font-size-h3)',
|
|
161
|
+
cursor: 'pointer',
|
|
162
|
+
padding: 'var(--spacing-1) var(--spacing-2)',
|
|
163
|
+
lineHeight: 1,
|
|
164
|
+
borderRadius: 'var(--radius-md)',
|
|
165
|
+
transition: 'var(--transition-colors)',
|
|
166
|
+
display: 'flex',
|
|
167
|
+
alignItems: 'center',
|
|
168
|
+
justifyContent: 'center',
|
|
169
|
+
}}
|
|
170
|
+
onMouseEnter={(e) => {
|
|
171
|
+
e.currentTarget.style.color = 'var(--color-error)';
|
|
172
|
+
e.currentTarget.style.background = 'var(--color-error-bg)';
|
|
173
|
+
}}
|
|
174
|
+
onMouseLeave={(e) => {
|
|
175
|
+
e.currentTarget.style.color = 'var(--color-text-secondary)';
|
|
176
|
+
e.currentTarget.style.background = 'transparent';
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
×
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Content */}
|
|
184
|
+
<div
|
|
185
|
+
style={{
|
|
186
|
+
flex: 1,
|
|
187
|
+
overflow: 'auto',
|
|
188
|
+
background: 'var(--color-bg-elevated)',
|
|
189
|
+
color: 'var(--color-text-primary)',
|
|
190
|
+
padding: 'var(--space-layout-md)',
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
{isLoading ? (
|
|
194
|
+
<div
|
|
195
|
+
style={{
|
|
196
|
+
textAlign: 'center',
|
|
197
|
+
padding: 'var(--space-layout-lg)',
|
|
198
|
+
color: 'var(--color-text-muted)',
|
|
199
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
200
|
+
fontFamily: 'var(--font-family-sans)',
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
Loading artifact details...
|
|
204
|
+
</div>
|
|
205
|
+
) : error ? (
|
|
206
|
+
<div
|
|
207
|
+
style={{
|
|
208
|
+
textAlign: 'center',
|
|
209
|
+
padding: 'var(--space-layout-lg)',
|
|
210
|
+
color: 'var(--color-error-light)',
|
|
211
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
212
|
+
fontFamily: 'var(--font-family-sans)',
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
Error: {error}
|
|
216
|
+
</div>
|
|
217
|
+
) : artifact ? (
|
|
218
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap-lg)' }}>
|
|
219
|
+
{/* Timestamp */}
|
|
220
|
+
<div>
|
|
221
|
+
<div
|
|
222
|
+
style={{
|
|
223
|
+
fontSize: 'var(--font-size-caption)',
|
|
224
|
+
color: 'var(--color-text-tertiary)',
|
|
225
|
+
fontFamily: 'var(--font-family-sans)',
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
{new Date(artifact.created_at).toLocaleString()}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Two-column layout: Metadata (left) + Payload (right) */}
|
|
233
|
+
<div
|
|
234
|
+
style={{
|
|
235
|
+
display: 'grid',
|
|
236
|
+
gridTemplateColumns: 'minmax(250px, 350px) 1fr',
|
|
237
|
+
gap: 'var(--gap-xl)',
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
{/* Metadata Section - Left Column */}
|
|
241
|
+
<div>
|
|
242
|
+
<h3
|
|
243
|
+
style={{
|
|
244
|
+
fontSize: 'var(--font-size-body)',
|
|
245
|
+
fontWeight: 'var(--font-weight-semibold)',
|
|
246
|
+
color: 'var(--color-text-primary)',
|
|
247
|
+
fontFamily: 'var(--font-family-sans)',
|
|
248
|
+
marginBottom: 'var(--space-component-md)',
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
METADATA
|
|
252
|
+
</h3>
|
|
253
|
+
<dl
|
|
254
|
+
style={{
|
|
255
|
+
display: 'grid',
|
|
256
|
+
gridTemplateColumns: 'auto 1fr',
|
|
257
|
+
gap: 'var(--gap-sm) var(--gap-lg)',
|
|
258
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
259
|
+
fontFamily: 'var(--font-family-sans)',
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
<dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
|
|
263
|
+
Produced By
|
|
264
|
+
</dt>
|
|
265
|
+
<dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>{artifact.produced_by}</dd>
|
|
266
|
+
|
|
267
|
+
<dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
|
|
268
|
+
Correlation
|
|
269
|
+
</dt>
|
|
270
|
+
<dd
|
|
271
|
+
style={{
|
|
272
|
+
color: 'var(--color-text-primary)',
|
|
273
|
+
margin: 0,
|
|
274
|
+
fontFamily: 'var(--font-family-mono)',
|
|
275
|
+
fontSize: 'var(--font-size-caption)',
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
{artifact.correlation_id || '—'}
|
|
279
|
+
</dd>
|
|
280
|
+
|
|
281
|
+
<dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
|
|
282
|
+
Partition
|
|
283
|
+
</dt>
|
|
284
|
+
<dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>
|
|
285
|
+
{artifact.partition_key || '—'}
|
|
286
|
+
</dd>
|
|
287
|
+
|
|
288
|
+
<dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
|
|
289
|
+
Tags
|
|
290
|
+
</dt>
|
|
291
|
+
<dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>
|
|
292
|
+
{artifact.tags.length > 0 ? artifact.tags.join(', ') : '—'}
|
|
293
|
+
</dd>
|
|
294
|
+
|
|
295
|
+
<dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
|
|
296
|
+
Visibility
|
|
297
|
+
</dt>
|
|
298
|
+
<dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>
|
|
299
|
+
{artifact.visibility_kind || artifact.visibility?.kind || 'Unknown'}
|
|
300
|
+
</dd>
|
|
301
|
+
|
|
302
|
+
<dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
|
|
303
|
+
Consumed By
|
|
304
|
+
</dt>
|
|
305
|
+
<dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>
|
|
306
|
+
{artifact.consumed_by && artifact.consumed_by.length > 0
|
|
307
|
+
? artifact.consumed_by.join(', ')
|
|
308
|
+
: '—'}
|
|
309
|
+
</dd>
|
|
310
|
+
</dl>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{/* Payload Section - Right Column */}
|
|
314
|
+
<div>
|
|
315
|
+
<h3
|
|
316
|
+
style={{
|
|
317
|
+
fontSize: 'var(--font-size-body)',
|
|
318
|
+
fontWeight: 'var(--font-weight-semibold)',
|
|
319
|
+
color: 'var(--color-text-primary)',
|
|
320
|
+
fontFamily: 'var(--font-family-sans)',
|
|
321
|
+
marginBottom: 'var(--space-component-md)',
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
PAYLOAD
|
|
325
|
+
</h3>
|
|
326
|
+
<div
|
|
327
|
+
style={{
|
|
328
|
+
background: 'var(--color-bg-base)',
|
|
329
|
+
border: 'var(--border-default)',
|
|
330
|
+
borderRadius: 'var(--radius-md)',
|
|
331
|
+
padding: 'var(--space-component-md)',
|
|
332
|
+
fontFamily: 'var(--font-family-mono)',
|
|
333
|
+
fontSize: 'var(--font-size-caption)',
|
|
334
|
+
maxHeight: '400px',
|
|
335
|
+
overflow: 'auto',
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
<JsonAttributeRenderer
|
|
339
|
+
value={JSON.stringify(artifact.payload, null, 2)}
|
|
340
|
+
maxStringLength={Number.POSITIVE_INFINITY}
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* Consumption History Section - Full Width Below */}
|
|
347
|
+
<div>
|
|
348
|
+
<h3
|
|
349
|
+
style={{
|
|
350
|
+
fontSize: 'var(--font-size-body)',
|
|
351
|
+
fontWeight: 'var(--font-weight-semibold)',
|
|
352
|
+
color: 'var(--color-text-primary)',
|
|
353
|
+
fontFamily: 'var(--font-family-sans)',
|
|
354
|
+
marginBottom: 'var(--space-component-md)',
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
CONSUMPTION HISTORY
|
|
358
|
+
</h3>
|
|
359
|
+
{artifact.consumptions && artifact.consumptions.length > 0 ? (
|
|
360
|
+
<ul
|
|
361
|
+
style={{
|
|
362
|
+
listStyle: 'none',
|
|
363
|
+
padding: 0,
|
|
364
|
+
margin: 0,
|
|
365
|
+
display: 'flex',
|
|
366
|
+
flexDirection: 'column',
|
|
367
|
+
gap: 'var(--gap-md)',
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
{artifact.consumptions.map((entry) => (
|
|
371
|
+
<li
|
|
372
|
+
key={`${entry.consumer}-${entry.consumed_at}`}
|
|
373
|
+
style={{
|
|
374
|
+
padding: 'var(--space-component-md)',
|
|
375
|
+
background: 'var(--color-bg-surface)',
|
|
376
|
+
border: 'var(--border-default)',
|
|
377
|
+
borderRadius: 'var(--radius-md)',
|
|
378
|
+
fontFamily: 'var(--font-family-sans)',
|
|
379
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
<div
|
|
383
|
+
style={{
|
|
384
|
+
fontWeight: 'var(--font-weight-semibold)',
|
|
385
|
+
color: 'var(--color-text-primary)',
|
|
386
|
+
marginBottom: 'var(--spacing-1)',
|
|
387
|
+
}}
|
|
388
|
+
>
|
|
389
|
+
{entry.consumer}
|
|
390
|
+
</div>
|
|
391
|
+
<div
|
|
392
|
+
style={{
|
|
393
|
+
fontSize: 'var(--font-size-caption)',
|
|
394
|
+
color: 'var(--color-text-tertiary)',
|
|
395
|
+
}}
|
|
396
|
+
>
|
|
397
|
+
{new Date(entry.consumed_at).toLocaleString()}
|
|
398
|
+
</div>
|
|
399
|
+
{entry.run_id && (
|
|
400
|
+
<div
|
|
401
|
+
style={{
|
|
402
|
+
display: 'inline-block',
|
|
403
|
+
marginTop: 'var(--spacing-2)',
|
|
404
|
+
padding: 'var(--spacing-1) var(--spacing-2)',
|
|
405
|
+
background: 'var(--color-primary-900)',
|
|
406
|
+
color: 'var(--color-primary-300)',
|
|
407
|
+
borderRadius: 'var(--radius-sm)',
|
|
408
|
+
fontSize: 'var(--font-size-overline)',
|
|
409
|
+
fontFamily: 'var(--font-family-mono)',
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
Run {entry.run_id}
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
</li>
|
|
416
|
+
))}
|
|
417
|
+
</ul>
|
|
418
|
+
) : (
|
|
419
|
+
<p
|
|
420
|
+
style={{
|
|
421
|
+
color: 'var(--color-text-muted)',
|
|
422
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
423
|
+
fontFamily: 'var(--font-family-sans)',
|
|
424
|
+
fontStyle: 'italic',
|
|
425
|
+
}}
|
|
426
|
+
>
|
|
427
|
+
No consumers recorded for this artifact.
|
|
428
|
+
</p>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
) : null}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
</Rnd>
|
|
436
|
+
);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
export default MessageDetailWindow;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
2
2
|
import { useGraphStore } from '../../store/graphStore';
|
|
3
3
|
|
|
4
4
|
interface MessageHistoryTabProps {
|
|
@@ -12,70 +12,119 @@ interface MessageHistoryEntry {
|
|
|
12
12
|
direction: 'consumed' | 'published';
|
|
13
13
|
payload: any;
|
|
14
14
|
timestamp: number;
|
|
15
|
-
correlationId: string;
|
|
15
|
+
correlationId: string | null;
|
|
16
|
+
produced_by?: string;
|
|
17
|
+
consumed_at?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const MessageHistoryTab: React.FC<MessageHistoryTabProps> = ({ nodeId, nodeType }) => {
|
|
19
|
-
const
|
|
20
|
-
const agents = useGraphStore((state) => state.agents);
|
|
20
|
+
const MessageHistoryTab: React.FC<MessageHistoryTabProps> = ({ nodeId, nodeType: _nodeType }) => {
|
|
21
|
+
const [messageHistory, setMessageHistory] = useState<MessageHistoryEntry[]>([]);
|
|
21
22
|
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
|
|
23
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const isInitialLoad = useRef(true);
|
|
22
26
|
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
const history: MessageHistoryEntry[] = [];
|
|
27
|
+
// Subscribe to real-time events for this node
|
|
28
|
+
const events = useGraphStore((state) => state.events);
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
// Phase 4.1 Feature Gap Fix: Fetch complete message history from backend API
|
|
31
|
+
// Includes both produced AND consumed messages for the node
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const fetchMessageHistory = async () => {
|
|
34
|
+
// Only show loading spinner on initial load
|
|
35
|
+
if (isInitialLoad.current) {
|
|
36
|
+
setIsLoading(true);
|
|
37
|
+
}
|
|
38
|
+
setError(null);
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(`/api/artifacts/history/${nodeId}`);
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`Failed to fetch message history: ${response.statusText}`);
|
|
44
|
+
}
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
const isPublished = message.producedBy === nodeId;
|
|
46
|
+
const data = await response.json();
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Convert ISO timestamps to milliseconds
|
|
49
|
+
const history = data.messages.map((msg: any) => ({
|
|
50
|
+
id: msg.id,
|
|
51
|
+
type: msg.type,
|
|
52
|
+
direction: msg.direction,
|
|
53
|
+
payload: msg.payload,
|
|
54
|
+
timestamp: new Date(msg.consumed_at || msg.timestamp).getTime(),
|
|
55
|
+
correlationId: msg.correlation_id,
|
|
56
|
+
produced_by: msg.produced_by,
|
|
57
|
+
consumed_at: msg.consumed_at,
|
|
58
|
+
}));
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
60
|
+
setMessageHistory(history);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('Failed to fetch message history:', err);
|
|
63
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
64
|
+
} finally {
|
|
65
|
+
if (isInitialLoad.current) {
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
isInitialLoad.current = false;
|
|
59
68
|
}
|
|
60
|
-
});
|
|
61
|
-
} else if (nodeType === 'message') {
|
|
62
|
-
// For message nodes, just show that single message
|
|
63
|
-
const message = messages.get(nodeId);
|
|
64
|
-
if (message) {
|
|
65
|
-
history.push({
|
|
66
|
-
id: message.id,
|
|
67
|
-
type: message.type,
|
|
68
|
-
direction: 'published',
|
|
69
|
-
payload: message.payload,
|
|
70
|
-
timestamp: message.timestamp,
|
|
71
|
-
correlationId: message.correlationId,
|
|
72
|
-
});
|
|
73
69
|
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
fetchMessageHistory();
|
|
73
|
+
}, [nodeId]);
|
|
74
|
+
|
|
75
|
+
// Real-time updates: Refetch when new events arrive for this node
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
// Debounce refetch to avoid spamming API
|
|
78
|
+
let refetchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
79
|
+
|
|
80
|
+
const scheduleRefetch = () => {
|
|
81
|
+
if (refetchTimer !== null) {
|
|
82
|
+
clearTimeout(refetchTimer);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
refetchTimer = setTimeout(async () => {
|
|
86
|
+
refetchTimer = null;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(`/api/artifacts/history/${nodeId}`);
|
|
90
|
+
if (!response.ok) return;
|
|
91
|
+
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
|
|
94
|
+
const history = data.messages.map((msg: any) => ({
|
|
95
|
+
id: msg.id,
|
|
96
|
+
type: msg.type,
|
|
97
|
+
direction: msg.direction,
|
|
98
|
+
payload: msg.payload,
|
|
99
|
+
timestamp: new Date(msg.consumed_at || msg.timestamp).getTime(),
|
|
100
|
+
correlationId: msg.correlation_id,
|
|
101
|
+
produced_by: msg.produced_by,
|
|
102
|
+
consumed_at: msg.consumed_at,
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
setMessageHistory(history);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error('Failed to refetch message history:', err);
|
|
108
|
+
}
|
|
109
|
+
}, 500); // 500ms debounce
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Check if any recent event relates to this node
|
|
113
|
+
const recentEvents = events.slice(-10); // Check last 10 events
|
|
114
|
+
const hasRelevantEvent = recentEvents.some(
|
|
115
|
+
(event: any) => event.producedBy === nodeId || event.consumedBy === nodeId
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (hasRelevantEvent) {
|
|
119
|
+
scheduleRefetch();
|
|
74
120
|
}
|
|
75
121
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
122
|
+
return () => {
|
|
123
|
+
if (refetchTimer !== null) {
|
|
124
|
+
clearTimeout(refetchTimer);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}, [events, nodeId]);
|
|
79
128
|
|
|
80
129
|
const formatTimestamp = (timestamp: number) => {
|
|
81
130
|
return new Date(timestamp).toLocaleString();
|
|
@@ -111,7 +160,33 @@ const MessageHistoryTab: React.FC<MessageHistoryTabProps> = ({ nodeId, nodeType
|
|
|
111
160
|
color: 'var(--color-text-primary)',
|
|
112
161
|
}}
|
|
113
162
|
>
|
|
114
|
-
{
|
|
163
|
+
{isLoading ? (
|
|
164
|
+
<div
|
|
165
|
+
data-testid="loading-messages"
|
|
166
|
+
style={{
|
|
167
|
+
padding: 'var(--space-layout-md)',
|
|
168
|
+
color: 'var(--color-text-muted)',
|
|
169
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
170
|
+
fontFamily: 'var(--font-family-sans)',
|
|
171
|
+
textAlign: 'center',
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
Loading message history...
|
|
175
|
+
</div>
|
|
176
|
+
) : error ? (
|
|
177
|
+
<div
|
|
178
|
+
data-testid="error-messages"
|
|
179
|
+
style={{
|
|
180
|
+
padding: 'var(--space-layout-md)',
|
|
181
|
+
color: 'var(--color-error-light)',
|
|
182
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
183
|
+
fontFamily: 'var(--font-family-sans)',
|
|
184
|
+
textAlign: 'center',
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
Error: {error}
|
|
188
|
+
</div>
|
|
189
|
+
) : messageHistory.length === 0 ? (
|
|
115
190
|
<div
|
|
116
191
|
data-testid="empty-messages"
|
|
117
192
|
style={{
|