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.

Files changed (49) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE +21 -0
  3. package/README.md +722 -0
  4. package/cleanup.js +73 -0
  5. package/config.js +50 -0
  6. package/dist/assets/icons-Ijf8rQIc.js +1 -0
  7. package/dist/assets/index-Cqc1m1x_.css +1 -0
  8. package/dist/assets/index-jGy3ms0W.js +9 -0
  9. package/dist/assets/react-vendor-DbmSkCAF.js +1 -0
  10. package/dist/index.html +16 -0
  11. package/index.html +13 -0
  12. package/package.json +93 -0
  13. package/server.js +953 -0
  14. package/src/App.jsx +372 -0
  15. package/src/animations-enhanced.css +929 -0
  16. package/src/animations.css +783 -0
  17. package/src/components/ActivityFeed.jsx +289 -0
  18. package/src/components/AgentActivity.jsx +104 -0
  19. package/src/components/AgentCard.jsx +163 -0
  20. package/src/components/AgentOutputViewer.jsx +334 -0
  21. package/src/components/ArchiveViewer.jsx +283 -0
  22. package/src/components/ConnectionStatus.jsx +124 -0
  23. package/src/components/DetailedTaskProgress.jsx +126 -0
  24. package/src/components/ErrorBoundary.jsx +132 -0
  25. package/src/components/Header.jsx +154 -0
  26. package/src/components/LiveAgentStream.jsx +176 -0
  27. package/src/components/LiveCommunication.jsx +326 -0
  28. package/src/components/LiveMetrics.jsx +100 -0
  29. package/src/components/RealTimeMessages.jsx +298 -0
  30. package/src/components/SkeletonLoader.jsx +384 -0
  31. package/src/components/StatsOverview.jsx +209 -0
  32. package/src/components/SystemStatus.jsx +57 -0
  33. package/src/components/TaskList.jsx +306 -0
  34. package/src/components/TeamCard.jsx +126 -0
  35. package/src/components/TeamHistory.jsx +204 -0
  36. package/src/components/__tests__/ConnectionStatus.test.jsx +54 -0
  37. package/src/components/__tests__/StatsOverview.test.jsx +66 -0
  38. package/src/config/constants.js +59 -0
  39. package/src/hooks/useCounterAnimation.js +219 -0
  40. package/src/hooks/useWebSocket.js +76 -0
  41. package/src/index.css +1818 -0
  42. package/src/main.jsx +17 -0
  43. package/src/polish-enhancements.css +303 -0
  44. package/src/premium-visual-polish.css +830 -0
  45. package/src/responsive-enhancements.css +666 -0
  46. package/src/styles/theme.css +395 -0
  47. package/src/test/setup.js +19 -0
  48. package/start.js +36 -0
  49. 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
+ };