claude-team-dashboard 1.2.2
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 claude-team-dashboard might be problematic. Click here for more details.
- package/CHANGELOG.md +76 -0
- package/LICENSE +21 -0
- package/README.md +722 -0
- package/cleanup.js +73 -0
- package/config.js +50 -0
- package/dist/assets/icons-Ijf8rQIc.js +1 -0
- package/dist/assets/index-Cqc1m1x_.css +1 -0
- package/dist/assets/index-jGy3ms0W.js +9 -0
- package/dist/assets/react-vendor-DbmSkCAF.js +1 -0
- package/dist/index.html +16 -0
- package/index.html +13 -0
- package/package.json +93 -0
- package/server.js +953 -0
- package/src/App.jsx +372 -0
- package/src/animations-enhanced.css +929 -0
- package/src/animations.css +783 -0
- package/src/components/ActivityFeed.jsx +289 -0
- package/src/components/AgentActivity.jsx +104 -0
- package/src/components/AgentCard.jsx +163 -0
- package/src/components/AgentOutputViewer.jsx +334 -0
- package/src/components/ArchiveViewer.jsx +283 -0
- package/src/components/ConnectionStatus.jsx +124 -0
- package/src/components/DetailedTaskProgress.jsx +126 -0
- package/src/components/ErrorBoundary.jsx +132 -0
- package/src/components/Header.jsx +154 -0
- package/src/components/LiveAgentStream.jsx +176 -0
- package/src/components/LiveCommunication.jsx +326 -0
- package/src/components/LiveMetrics.jsx +100 -0
- package/src/components/RealTimeMessages.jsx +298 -0
- package/src/components/SkeletonLoader.jsx +384 -0
- package/src/components/StatsOverview.jsx +209 -0
- package/src/components/SystemStatus.jsx +57 -0
- package/src/components/TaskList.jsx +306 -0
- package/src/components/TeamCard.jsx +126 -0
- package/src/components/TeamHistory.jsx +204 -0
- package/src/components/__tests__/ConnectionStatus.test.jsx +54 -0
- package/src/components/__tests__/StatsOverview.test.jsx +66 -0
- package/src/config/constants.js +59 -0
- package/src/hooks/useCounterAnimation.js +219 -0
- package/src/hooks/useWebSocket.js +76 -0
- package/src/index.css +1818 -0
- package/src/main.jsx +17 -0
- package/src/polish-enhancements.css +303 -0
- package/src/premium-visual-polish.css +830 -0
- package/src/responsive-enhancements.css +666 -0
- package/src/styles/theme.css +395 -0
- package/src/test/setup.js +19 -0
- package/start.js +36 -0
- package/vite.config.js +37 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { Terminal, Maximize2, Minimize2, Download, RefreshCw, AlertCircle } from 'lucide-react';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
5
|
+
dayjs.extend(relativeTime);
|
|
6
|
+
|
|
7
|
+
export function AgentOutputViewer({ agentOutputs }) {
|
|
8
|
+
const [selectedOutput, setSelectedOutput] = useState(null);
|
|
9
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
10
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
11
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
12
|
+
const [refreshError, setRefreshError] = useState(null);
|
|
13
|
+
const [localOutputs, setLocalOutputs] = useState(agentOutputs || []);
|
|
14
|
+
const outputRef = useRef(null);
|
|
15
|
+
const pollingIntervalRef = useRef(null);
|
|
16
|
+
|
|
17
|
+
// Fetch outputs from API
|
|
18
|
+
const fetchOutputs = useCallback(async () => {
|
|
19
|
+
try {
|
|
20
|
+
setIsRefreshing(true);
|
|
21
|
+
setRefreshError(null);
|
|
22
|
+
|
|
23
|
+
const response = await fetch(`http://${window.location.hostname}:3001/api/agent-outputs`);
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
|
|
31
|
+
if (data.outputs && Array.isArray(data.outputs)) {
|
|
32
|
+
setLocalOutputs(data.outputs);
|
|
33
|
+
return data.outputs;
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error('Invalid response format');
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error fetching agent outputs:', error);
|
|
39
|
+
setRefreshError(error.message);
|
|
40
|
+
return null;
|
|
41
|
+
} finally {
|
|
42
|
+
setIsRefreshing(false);
|
|
43
|
+
}
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
// Manual refresh handler
|
|
47
|
+
const handleManualRefresh = useCallback(async () => {
|
|
48
|
+
await fetchOutputs();
|
|
49
|
+
}, [fetchOutputs]);
|
|
50
|
+
|
|
51
|
+
// Use WebSocket data when available, fallback to polling
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (agentOutputs && agentOutputs.length > 0) {
|
|
54
|
+
setLocalOutputs(agentOutputs);
|
|
55
|
+
setRefreshError(null);
|
|
56
|
+
}
|
|
57
|
+
}, [agentOutputs]);
|
|
58
|
+
|
|
59
|
+
// Fallback polling mechanism (only if no WebSocket data)
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
// If we haven't received any data via WebSocket, start polling
|
|
62
|
+
if (!agentOutputs || agentOutputs.length === 0) {
|
|
63
|
+
// Initial fetch
|
|
64
|
+
fetchOutputs();
|
|
65
|
+
|
|
66
|
+
// Poll every 5 seconds as fallback
|
|
67
|
+
pollingIntervalRef.current = setInterval(() => {
|
|
68
|
+
fetchOutputs();
|
|
69
|
+
}, 5000);
|
|
70
|
+
} else {
|
|
71
|
+
// Clear polling if WebSocket is working
|
|
72
|
+
if (pollingIntervalRef.current) {
|
|
73
|
+
clearInterval(pollingIntervalRef.current);
|
|
74
|
+
pollingIntervalRef.current = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
if (pollingIntervalRef.current) {
|
|
80
|
+
clearInterval(pollingIntervalRef.current);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}, [agentOutputs, fetchOutputs]);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
// Auto-select most recent output
|
|
87
|
+
if (localOutputs && localOutputs.length > 0 && !selectedOutput) {
|
|
88
|
+
setSelectedOutput(localOutputs[0]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Update selected output if it exists in new data
|
|
92
|
+
if (selectedOutput && localOutputs && localOutputs.length > 0) {
|
|
93
|
+
const updatedOutput = localOutputs.find(o => o.taskId === selectedOutput.taskId);
|
|
94
|
+
if (updatedOutput && updatedOutput.lastModified !== selectedOutput.lastModified) {
|
|
95
|
+
setSelectedOutput(updatedOutput);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}, [localOutputs, selectedOutput]);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
// Auto-scroll to bottom when content updates
|
|
102
|
+
if (autoScroll && outputRef.current) {
|
|
103
|
+
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
|
104
|
+
}
|
|
105
|
+
}, [selectedOutput, autoScroll]);
|
|
106
|
+
|
|
107
|
+
const downloadOutput = () => {
|
|
108
|
+
if (!selectedOutput) return;
|
|
109
|
+
|
|
110
|
+
const blob = new Blob([selectedOutput.content], { type: 'text/plain' });
|
|
111
|
+
const url = URL.createObjectURL(blob);
|
|
112
|
+
const a = document.createElement('a');
|
|
113
|
+
a.href = url;
|
|
114
|
+
a.download = `agent-${selectedOutput.taskId}-output.txt`;
|
|
115
|
+
document.body.appendChild(a);
|
|
116
|
+
a.click();
|
|
117
|
+
document.body.removeChild(a);
|
|
118
|
+
URL.revokeObjectURL(url);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const formatSize = (bytes) => {
|
|
122
|
+
if (bytes < 1024) return bytes + ' B';
|
|
123
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
124
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (!localOutputs || localOutputs.length === 0) {
|
|
128
|
+
return (
|
|
129
|
+
<div className="card">
|
|
130
|
+
<div className="flex items-center justify-between mb-4">
|
|
131
|
+
<div className="flex items-center gap-2">
|
|
132
|
+
<Terminal className="h-5 w-5 text-claude-orange" />
|
|
133
|
+
<h3 className="text-lg font-semibold text-white">Agent Output Stream</h3>
|
|
134
|
+
</div>
|
|
135
|
+
<button
|
|
136
|
+
onClick={handleManualRefresh}
|
|
137
|
+
disabled={isRefreshing}
|
|
138
|
+
className="p-2 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 transition-colors disabled:opacity-50"
|
|
139
|
+
aria-label="Refresh outputs"
|
|
140
|
+
title="Refresh outputs"
|
|
141
|
+
>
|
|
142
|
+
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{refreshError && (
|
|
147
|
+
<div className="mb-4 p-3 rounded-lg bg-red-900/20 border border-red-500/30 flex items-start gap-2">
|
|
148
|
+
<AlertCircle className="h-5 w-5 text-red-400 flex-shrink-0 mt-0.5" />
|
|
149
|
+
<div className="flex-1">
|
|
150
|
+
<p className="text-sm text-red-400 font-medium">Failed to load outputs</p>
|
|
151
|
+
<p className="text-xs text-red-400/80 mt-1">{refreshError}</p>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
<div className="text-center py-12 text-gray-400">
|
|
157
|
+
<Terminal className="h-16 w-16 mx-auto mb-3 opacity-50" />
|
|
158
|
+
<p className="text-sm">No agent outputs available</p>
|
|
159
|
+
<p className="text-xs mt-1">Real-time teammate outputs will stream here</p>
|
|
160
|
+
{isRefreshing && (
|
|
161
|
+
<p className="text-xs mt-2 text-claude-orange">Loading outputs...</p>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className={`card ${isExpanded ? 'fixed inset-4 z-50' : ''}`}>
|
|
170
|
+
{/* Header */}
|
|
171
|
+
<div className="flex items-center justify-between mb-4">
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<Terminal className="h-5 w-5 text-claude-orange animate-pulse" />
|
|
174
|
+
<h3 className="text-lg font-semibold text-white">Agent Output Stream</h3>
|
|
175
|
+
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 text-xs rounded-full border border-green-500/30 flex items-center gap-1">
|
|
176
|
+
<span className="h-2 w-2 rounded-full bg-green-400 animate-pulse"></span>
|
|
177
|
+
LIVE
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="flex items-center gap-2">
|
|
182
|
+
<button
|
|
183
|
+
onClick={handleManualRefresh}
|
|
184
|
+
disabled={isRefreshing}
|
|
185
|
+
className="p-2 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
186
|
+
aria-label="Refresh outputs"
|
|
187
|
+
title="Refresh outputs"
|
|
188
|
+
>
|
|
189
|
+
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
|
190
|
+
</button>
|
|
191
|
+
<button
|
|
192
|
+
onClick={() => setAutoScroll(!autoScroll)}
|
|
193
|
+
className={`px-3 py-2 rounded-lg text-xs font-medium transition-colors ${
|
|
194
|
+
autoScroll
|
|
195
|
+
? 'bg-green-500/20 text-green-400 border border-green-500/30'
|
|
196
|
+
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
197
|
+
}`}
|
|
198
|
+
aria-label={autoScroll ? "Auto-scroll enabled" : "Enable auto-scroll"}
|
|
199
|
+
title={autoScroll ? "Auto-scroll enabled" : "Enable auto-scroll"}
|
|
200
|
+
>
|
|
201
|
+
{autoScroll ? 'Auto-scroll ON' : 'Auto-scroll OFF'}
|
|
202
|
+
</button>
|
|
203
|
+
<button
|
|
204
|
+
onClick={downloadOutput}
|
|
205
|
+
disabled={!selectedOutput}
|
|
206
|
+
className="p-2 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
207
|
+
aria-label="Download agent output"
|
|
208
|
+
title="Download agent output"
|
|
209
|
+
>
|
|
210
|
+
<Download className="h-4 w-4" />
|
|
211
|
+
</button>
|
|
212
|
+
<button
|
|
213
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
214
|
+
className="p-2 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 transition-colors"
|
|
215
|
+
aria-label={isExpanded ? 'Minimize output viewer' : 'Maximize output viewer'}
|
|
216
|
+
aria-expanded={isExpanded}
|
|
217
|
+
title={isExpanded ? 'Minimize' : 'Maximize'}
|
|
218
|
+
>
|
|
219
|
+
{isExpanded ? (
|
|
220
|
+
<Minimize2 className="h-4 w-4" />
|
|
221
|
+
) : (
|
|
222
|
+
<Maximize2 className="h-4 w-4" />
|
|
223
|
+
)}
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{/* Error Display */}
|
|
229
|
+
{refreshError && (
|
|
230
|
+
<div className="mb-4 p-3 rounded-lg bg-yellow-900/20 border border-yellow-500/30 flex items-start gap-2">
|
|
231
|
+
<AlertCircle className="h-5 w-5 text-yellow-400 flex-shrink-0 mt-0.5" />
|
|
232
|
+
<div className="flex-1">
|
|
233
|
+
<p className="text-sm text-yellow-400 font-medium">Connection Issue</p>
|
|
234
|
+
<p className="text-xs text-yellow-400/80 mt-1">
|
|
235
|
+
{refreshError} - Displaying cached data or using fallback polling
|
|
236
|
+
</p>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
|
|
241
|
+
{/* Output Selector */}
|
|
242
|
+
<div className="mb-4">
|
|
243
|
+
<label className="block text-sm text-gray-400 mb-2">Select Agent Output:</label>
|
|
244
|
+
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
245
|
+
{localOutputs.map((output, index) => (
|
|
246
|
+
<button
|
|
247
|
+
key={output.taskId}
|
|
248
|
+
onClick={() => setSelectedOutput(output)}
|
|
249
|
+
className={`px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${
|
|
250
|
+
selectedOutput?.taskId === output.taskId
|
|
251
|
+
? 'bg-claude-orange text-white shadow-lg'
|
|
252
|
+
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
|
253
|
+
}`}
|
|
254
|
+
style={{
|
|
255
|
+
animation: `fadeInScale 0.3s ease-out ${index * 0.05}s backwards`
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<div className="flex items-center gap-2">
|
|
259
|
+
<Terminal className="h-4 w-4" />
|
|
260
|
+
<span>Task {output.taskId}</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="text-xs opacity-75 mt-0.5">
|
|
263
|
+
{dayjs(output.lastModified).fromNow()}
|
|
264
|
+
</div>
|
|
265
|
+
</button>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{/* Output Display */}
|
|
271
|
+
{selectedOutput && (
|
|
272
|
+
<>
|
|
273
|
+
{/* Output Info */}
|
|
274
|
+
<div className="mb-3 p-3 rounded-lg bg-gray-700/30 border border-gray-600/50">
|
|
275
|
+
<div className="flex items-center justify-between text-sm">
|
|
276
|
+
<div className="flex items-center gap-4">
|
|
277
|
+
<span className="text-gray-400">
|
|
278
|
+
Task ID: <span className="text-white font-mono">{selectedOutput.taskId}</span>
|
|
279
|
+
</span>
|
|
280
|
+
<span className="text-gray-400">
|
|
281
|
+
Size: <span className="text-white">{formatSize(selectedOutput.size)}</span>
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
<span className="text-gray-400">
|
|
285
|
+
Updated: <span className="text-white">{dayjs(selectedOutput.lastModified).fromNow()}</span>
|
|
286
|
+
</span>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Terminal Output */}
|
|
291
|
+
<div
|
|
292
|
+
ref={outputRef}
|
|
293
|
+
className={`font-mono text-sm bg-gray-900 rounded-lg p-4 overflow-auto border border-gray-700 ${
|
|
294
|
+
isExpanded ? 'h-[calc(100vh-300px)]' : 'h-[500px]'
|
|
295
|
+
}`}
|
|
296
|
+
onScroll={(e) => {
|
|
297
|
+
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
|
298
|
+
const isAtBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
|
|
299
|
+
if (!isAtBottom && autoScroll) {
|
|
300
|
+
setAutoScroll(false);
|
|
301
|
+
}
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{selectedOutput.content ? (
|
|
305
|
+
<pre className="text-green-400 whitespace-pre-wrap break-words">
|
|
306
|
+
{selectedOutput.content}
|
|
307
|
+
</pre>
|
|
308
|
+
) : (
|
|
309
|
+
<div className="text-gray-500 text-center py-12">
|
|
310
|
+
<Terminal className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
|
311
|
+
<p className="text-sm font-medium mb-2">No output content available</p>
|
|
312
|
+
<p className="text-xs text-gray-600 max-w-md mx-auto">
|
|
313
|
+
This task output is empty. Agent outputs will appear here when agents write to their output streams during task execution.
|
|
314
|
+
</p>
|
|
315
|
+
<p className="text-xs text-gray-600 mt-2">
|
|
316
|
+
Empty outputs are normal for agents that complete tasks quickly without verbose logging.
|
|
317
|
+
</p>
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
{/* Footer Info */}
|
|
323
|
+
<div className="mt-3 text-xs text-gray-400 text-center flex items-center justify-center gap-2">
|
|
324
|
+
<span className="h-2 w-2 rounded-full bg-green-400 animate-pulse"></span>
|
|
325
|
+
<span>
|
|
326
|
+
Real-time output from Claude Code agent • Updates via WebSocket
|
|
327
|
+
{!agentOutputs || agentOutputs.length === 0 ? ' (fallback polling active)' : ''}
|
|
328
|
+
</span>
|
|
329
|
+
</div>
|
|
330
|
+
</>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Archive, Calendar, Users, CheckCircle, Clock, TrendingUp, ChevronDown, ChevronUp, Package, FileText } from 'lucide-react';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
6
|
+
import duration from 'dayjs/plugin/duration';
|
|
7
|
+
|
|
8
|
+
dayjs.extend(relativeTime);
|
|
9
|
+
dayjs.extend(duration);
|
|
10
|
+
|
|
11
|
+
export function ArchiveViewer() {
|
|
12
|
+
const [archives, setArchives] = useState([]);
|
|
13
|
+
const [loading, setLoading] = useState(true);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
const [expandedArchive, setExpandedArchive] = useState(null);
|
|
16
|
+
const [selectedArchiveDetails, setSelectedArchiveDetails] = useState(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
fetchArchives();
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
const fetchArchives = async () => {
|
|
23
|
+
try {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
const response = await fetch('http://localhost:3001/api/archive');
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error('Failed to fetch archives');
|
|
28
|
+
}
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
setArchives(data.archives || []);
|
|
31
|
+
setError(null);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
setError(err.message);
|
|
34
|
+
console.error('Error fetching archives:', err);
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const fetchArchiveDetails = async (filename) => {
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(`http://localhost:3001/api/archive/${filename}`);
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error('Failed to fetch archive details');
|
|
45
|
+
}
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
setSelectedArchiveDetails(data);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('Error fetching archive details:', err);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const toggleExpand = async (filename) => {
|
|
54
|
+
if (expandedArchive === filename) {
|
|
55
|
+
setExpandedArchive(null);
|
|
56
|
+
setSelectedArchiveDetails(null);
|
|
57
|
+
} else {
|
|
58
|
+
setExpandedArchive(filename);
|
|
59
|
+
await fetchArchiveDetails(filename);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (loading) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="card">
|
|
66
|
+
<div className="flex items-center justify-center py-12">
|
|
67
|
+
<div className="text-center">
|
|
68
|
+
<Package className="h-12 w-12 text-gray-500 mx-auto mb-3 animate-pulse" />
|
|
69
|
+
<p className="text-gray-400">Loading archived teams...</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (error) {
|
|
77
|
+
return (
|
|
78
|
+
<div className="card border-l-4 border-l-red-500">
|
|
79
|
+
<div className="flex items-center gap-3 text-red-400">
|
|
80
|
+
<Archive className="h-6 w-6" />
|
|
81
|
+
<div>
|
|
82
|
+
<h3 className="font-semibold">Error Loading Archives</h3>
|
|
83
|
+
<p className="text-sm text-gray-400 mt-1">{error}</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (archives.length === 0) {
|
|
91
|
+
return (
|
|
92
|
+
<div className="card">
|
|
93
|
+
<div className="text-center py-12">
|
|
94
|
+
<Archive className="h-16 w-16 text-gray-600 mx-auto mb-4" />
|
|
95
|
+
<h3 className="text-xl font-semibold text-white mb-2">
|
|
96
|
+
No Archived Teams Yet
|
|
97
|
+
</h3>
|
|
98
|
+
<p className="text-gray-400">
|
|
99
|
+
When your agent teams complete their work, they'll be archived here for reference
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="space-y-4">
|
|
108
|
+
{/* Header */}
|
|
109
|
+
<div className="card border-l-4 border-l-purple-500 bg-gradient-to-r from-purple-900/20 to-transparent">
|
|
110
|
+
<div className="flex items-center justify-between">
|
|
111
|
+
<div className="flex items-center gap-3">
|
|
112
|
+
<div className="bg-purple-500/20 p-3 rounded-lg">
|
|
113
|
+
<Archive className="h-6 w-6 text-purple-400" />
|
|
114
|
+
</div>
|
|
115
|
+
<div>
|
|
116
|
+
<h2 className="text-2xl font-bold text-white">Team Archive</h2>
|
|
117
|
+
<p className="text-gray-400 text-sm mt-1">
|
|
118
|
+
History of completed agent teams and their accomplishments
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="bg-purple-500/20 px-4 py-2 rounded-lg">
|
|
123
|
+
<span className="text-2xl font-bold text-purple-400">{archives.length}</span>
|
|
124
|
+
<span className="text-sm text-gray-400 ml-2">archived</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* Archive List */}
|
|
130
|
+
{archives.map((archive, index) => (
|
|
131
|
+
<div
|
|
132
|
+
key={archive.filename}
|
|
133
|
+
className="card border-l-4 border-l-purple-500 hover:border-l-purple-400 transition-all duration-300"
|
|
134
|
+
>
|
|
135
|
+
{/* Archive Header */}
|
|
136
|
+
<div className="flex items-start justify-between mb-4">
|
|
137
|
+
<div className="flex-1">
|
|
138
|
+
<div className="flex items-center gap-3 mb-2">
|
|
139
|
+
<h3 className="text-xl font-bold text-white">{archive.overview?.split('"')[1] || 'Unknown Team'}</h3>
|
|
140
|
+
<span className="px-3 py-1 bg-purple-500/20 text-purple-400 text-xs font-medium rounded-full">
|
|
141
|
+
#{index + 1}
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
<p className="text-gray-300 text-sm leading-relaxed">{archive.overview}</p>
|
|
145
|
+
</div>
|
|
146
|
+
<button
|
|
147
|
+
onClick={() => toggleExpand(archive.filename)}
|
|
148
|
+
className="ml-4 p-2 hover:bg-gray-700 rounded-lg transition-colors flex-shrink-0"
|
|
149
|
+
aria-label={expandedArchive === archive.filename ? "Collapse details" : "Expand details"}
|
|
150
|
+
aria-expanded={expandedArchive === archive.filename}
|
|
151
|
+
>
|
|
152
|
+
{expandedArchive === archive.filename ? (
|
|
153
|
+
<ChevronUp className="h-5 w-5 text-gray-400" />
|
|
154
|
+
) : (
|
|
155
|
+
<ChevronDown className="h-5 w-5 text-gray-400" />
|
|
156
|
+
)}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Archive Stats */}
|
|
161
|
+
<div className="flex flex-wrap gap-3 mb-4">
|
|
162
|
+
<div className="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg">
|
|
163
|
+
<Calendar className="h-4 w-4 text-purple-400" />
|
|
164
|
+
<span className="text-sm text-gray-300">
|
|
165
|
+
{dayjs(archive.archivedAt).format('MMM D, YYYY')}
|
|
166
|
+
</span>
|
|
167
|
+
<span className="text-xs text-gray-500">
|
|
168
|
+
({dayjs(archive.archivedAt).fromNow()})
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{archive.members && (
|
|
173
|
+
<div className="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg">
|
|
174
|
+
<Users className="h-4 w-4 text-blue-400" />
|
|
175
|
+
<span className="text-sm text-gray-300">
|
|
176
|
+
{Array.isArray(archive.members) ? archive.members.length : 0} members
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{archive.accomplishments && (
|
|
182
|
+
<div className="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg">
|
|
183
|
+
<CheckCircle className="h-4 w-4 text-green-400" />
|
|
184
|
+
<span className="text-sm text-gray-300">
|
|
185
|
+
{archive.accomplishments.length} completed
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{archive.duration && (
|
|
191
|
+
<div className="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg">
|
|
192
|
+
<Clock className="h-4 w-4 text-orange-400" />
|
|
193
|
+
<span className="text-sm text-gray-300">
|
|
194
|
+
{archive.duration}
|
|
195
|
+
</span>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Quick Summary */}
|
|
201
|
+
{archive.created && (
|
|
202
|
+
<div className="bg-gray-800/50 rounded-lg p-3 mb-4">
|
|
203
|
+
<p className="text-sm text-gray-400 flex items-center gap-2">
|
|
204
|
+
<TrendingUp className="h-4 w-4 text-purple-400" />
|
|
205
|
+
{archive.created}
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Expanded Details */}
|
|
211
|
+
{expandedArchive === archive.filename && (
|
|
212
|
+
<div className="mt-4 pt-4 border-t border-gray-700 space-y-4 animate-fadeIn">
|
|
213
|
+
{/* Team Members */}
|
|
214
|
+
{archive.members && archive.members.length > 0 && (
|
|
215
|
+
<div>
|
|
216
|
+
<h4 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
|
217
|
+
<Users className="h-5 w-5 text-blue-400" />
|
|
218
|
+
Team Members
|
|
219
|
+
</h4>
|
|
220
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
221
|
+
{archive.members.map((member, idx) => (
|
|
222
|
+
<div key={idx} className="bg-gray-800/50 rounded-lg p-3 flex items-center gap-3">
|
|
223
|
+
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold text-sm">
|
|
224
|
+
{member.split(' ')[0]?.[0] || '?'}
|
|
225
|
+
</div>
|
|
226
|
+
<span className="text-sm text-gray-300">{member}</span>
|
|
227
|
+
</div>
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{/* Accomplishments */}
|
|
234
|
+
{archive.accomplishments && archive.accomplishments.length > 0 && (
|
|
235
|
+
<div>
|
|
236
|
+
<h4 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
|
237
|
+
<CheckCircle className="h-5 w-5 text-green-400" />
|
|
238
|
+
Key Accomplishments
|
|
239
|
+
</h4>
|
|
240
|
+
<div className="space-y-2">
|
|
241
|
+
{archive.accomplishments.map((accomplishment, idx) => (
|
|
242
|
+
<div key={idx} className="bg-gray-800/50 rounded-lg p-3 flex items-start gap-3">
|
|
243
|
+
<CheckCircle className="h-4 w-4 text-green-400 mt-0.5 flex-shrink-0" />
|
|
244
|
+
<span className="text-sm text-gray-300 leading-relaxed">
|
|
245
|
+
{accomplishment.replace('✅ ', '')}
|
|
246
|
+
</span>
|
|
247
|
+
</div>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Full Details Link */}
|
|
254
|
+
{selectedArchiveDetails && (
|
|
255
|
+
<div className="bg-purple-900/20 rounded-lg p-4 border border-purple-500/30">
|
|
256
|
+
<div className="flex items-center gap-2 mb-2">
|
|
257
|
+
<FileText className="h-4 w-4 text-purple-400" />
|
|
258
|
+
<h4 className="text-sm font-semibold text-purple-400">Full Archive Data</h4>
|
|
259
|
+
</div>
|
|
260
|
+
<p className="text-xs text-gray-400 mb-2">
|
|
261
|
+
Team: {selectedArchiveDetails.teamName}
|
|
262
|
+
</p>
|
|
263
|
+
<p className="text-xs text-gray-400">
|
|
264
|
+
Total Tasks: {selectedArchiveDetails.rawData?.tasks?.length || 0}
|
|
265
|
+
</p>
|
|
266
|
+
{selectedArchiveDetails.rawData?.config && (
|
|
267
|
+
<p className="text-xs text-gray-400">
|
|
268
|
+
Configuration: {selectedArchiveDetails.rawData.config.members?.length || 0} members configured
|
|
269
|
+
</p>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
ArchiveViewer.propTypes = {
|
|
282
|
+
// No props needed - fetches its own data
|
|
283
|
+
};
|