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,176 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Activity, Cpu, Zap } from 'lucide-react';
3
+ import dayjs from 'dayjs';
4
+ import relativeTime from 'dayjs/plugin/relativeTime';
5
+ dayjs.extend(relativeTime);
6
+
7
+ export function LiveAgentStream({ teams }) {
8
+ const [activities, setActivities] = useState([]);
9
+
10
+ useEffect(() => {
11
+ if (!teams || teams.length === 0) return;
12
+
13
+ const interval = setInterval(() => {
14
+ teams.forEach(team => {
15
+ if (team.config && team.config.members && team.tasks) {
16
+ team.config.members.forEach(member => {
17
+ const memberTasks = team.tasks.filter(t => t.owner === member.name);
18
+ const inProgressTask = memberTasks.find(t => t.status === 'in_progress');
19
+
20
+ if (inProgressTask && Math.random() > 0.7) {
21
+ const actions = [
22
+ 'Analyzing requirements',
23
+ 'Writing code',
24
+ 'Testing implementation',
25
+ 'Reviewing changes',
26
+ 'Optimizing performance',
27
+ 'Debugging issue',
28
+ 'Updating documentation',
29
+ 'Refactoring code',
30
+ 'Running tests',
31
+ 'Committing changes'
32
+ ];
33
+
34
+ const action = actions[Math.floor(Math.random() * actions.length)];
35
+
36
+ addActivity({
37
+ agent: member.name,
38
+ agentType: member.agentType,
39
+ team: team.name,
40
+ action: action,
41
+ task: inProgressTask.subject,
42
+ timestamp: new Date(),
43
+ progress: Math.floor(Math.random() * 100)
44
+ });
45
+ }
46
+ });
47
+ }
48
+ });
49
+ }, 2000);
50
+
51
+ return () => clearInterval(interval);
52
+ }, [teams]);
53
+
54
+ const addActivity = (activity) => {
55
+ setActivities(prev => [
56
+ { ...activity, id: Date.now() + Math.random() },
57
+ ...prev
58
+ ].slice(0, 100));
59
+ };
60
+
61
+ const getActivityColor = (agentType) => {
62
+ const colors = {
63
+ 'Front-End-Developer': '#60a5fa',
64
+ 'Animator': '#c084fc',
65
+ 'general-purpose': '#34d399',
66
+ 'Opus 4.6': '#fbbf24',
67
+ 'Sonnet 4.5': '#f97316'
68
+ };
69
+ return colors[agentType] || '#9ca3af';
70
+ };
71
+
72
+ return (
73
+ <div className="card" style={{ height: '600px', display: 'flex', flexDirection: 'column' }}>
74
+ <div className="flex items-center justify-between mb-4">
75
+ <div className="flex items-center gap-2">
76
+ <Activity className="h-5 w-5 text-claude-orange animate-pulse" />
77
+ <h3 className="text-lg font-semibold text-white">Live Agent Activity Stream</h3>
78
+ </div>
79
+ <div className="flex items-center gap-2">
80
+ <span className="text-xs text-green-400 flex items-center gap-1">
81
+ <Zap className="h-3 w-3 animate-pulse" />
82
+ LIVE
83
+ </span>
84
+ </div>
85
+ </div>
86
+
87
+ {/* Activity Stream */}
88
+ <div className="flex-1 overflow-y-auto space-y-1" style={{ minHeight: 0 }}>
89
+ {activities.length === 0 ? (
90
+ <div className="text-center py-12 text-gray-400">
91
+ <Activity className="h-16 w-16 mx-auto mb-3 opacity-50 animate-pulse" />
92
+ <p className="text-sm">Waiting for agent activity...</p>
93
+ <p className="text-xs mt-1">Live stream will appear here</p>
94
+ </div>
95
+ ) : (
96
+ activities.map(activity => (
97
+ <div
98
+ key={activity.id}
99
+ className="p-2 rounded-lg bg-gray-700/30 border border-gray-600/50 hover:border-claude-orange/50 transition-all"
100
+ style={{
101
+ animation: 'fadeIn 0.3s ease-out',
102
+ borderLeftWidth: '3px',
103
+ borderLeftColor: getActivityColor(activity.agentType)
104
+ }}
105
+ >
106
+ <div className="flex items-start gap-2">
107
+ <Cpu
108
+ className="h-4 w-4 mt-0.5 flex-shrink-0"
109
+ style={{ color: getActivityColor(activity.agentType) }}
110
+ />
111
+ <div className="flex-1 min-w-0">
112
+ <div className="flex items-center gap-2 mb-0.5">
113
+ <span
114
+ className="text-xs font-bold"
115
+ style={{ color: getActivityColor(activity.agentType) }}
116
+ >
117
+ {activity.agent}
118
+ </span>
119
+ <span className="text-xs text-gray-500">•</span>
120
+ <span className="text-xs text-gray-400 truncate">
121
+ {activity.action}
122
+ </span>
123
+ <span className="ml-auto text-xs text-gray-500 flex-shrink-0">
124
+ {dayjs(activity.timestamp).fromNow()}
125
+ </span>
126
+ </div>
127
+ <div className="text-xs text-gray-500 truncate">
128
+ {activity.task}
129
+ </div>
130
+ {activity.progress !== undefined && (
131
+ <div className="mt-1 flex items-center gap-2">
132
+ <div className="flex-1 bg-gray-600 rounded-full h-1">
133
+ <div
134
+ className="h-1 rounded-full transition-all duration-500"
135
+ style={{
136
+ width: `${activity.progress}%`,
137
+ backgroundColor: getActivityColor(activity.agentType)
138
+ }}
139
+ ></div>
140
+ </div>
141
+ <span className="text-xs text-gray-500 flex-shrink-0">
142
+ {activity.progress}%
143
+ </span>
144
+ </div>
145
+ )}
146
+ </div>
147
+ </div>
148
+ </div>
149
+ ))
150
+ )}
151
+ </div>
152
+
153
+ {/* Live Stats */}
154
+ <div className="pt-3 mt-3 border-t border-gray-700">
155
+ <div className="grid grid-cols-3 gap-2 text-center">
156
+ <div className="p-2 rounded-lg bg-blue-500/10">
157
+ <div className="text-lg font-bold text-blue-400">{activities.length}</div>
158
+ <div className="text-xs text-gray-400">Events</div>
159
+ </div>
160
+ <div className="p-2 rounded-lg bg-green-500/10">
161
+ <div className="text-lg font-bold text-green-400">
162
+ {new Set(activities.map(a => a.agent)).size}
163
+ </div>
164
+ <div className="text-xs text-gray-400">Active</div>
165
+ </div>
166
+ <div className="p-2 rounded-lg bg-purple-500/10">
167
+ <div className="text-lg font-bold text-purple-400">
168
+ {activities.filter(a => Date.now() - a.timestamp < 10000).length}
169
+ </div>
170
+ <div className="text-xs text-gray-400">Last 10s</div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ );
176
+ }
@@ -0,0 +1,326 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { MessageSquare, Users, Clock, Bot, CheckCircle, AlertCircle, Zap } from 'lucide-react';
3
+ import dayjs from 'dayjs';
4
+ import relativeTime from 'dayjs/plugin/relativeTime';
5
+ dayjs.extend(relativeTime);
6
+
7
+ // Convert technical messages to natural language
8
+ const parseMessageToNatural = (text, summary) => {
9
+ // If there's a summary, prefer it
10
+ if (summary && !summary.includes('{') && !summary.includes('idle_notification')) {
11
+ return { text: summary, type: 'message', icon: 'message' };
12
+ }
13
+
14
+ // Try to parse as JSON first
15
+ try {
16
+ const parsed = JSON.parse(text);
17
+
18
+ // Handle different message types
19
+ switch (parsed.type) {
20
+ case 'idle_notification':
21
+ return {
22
+ text: parsed.lastTaskSubject
23
+ ? `💤 Finished "${parsed.lastTaskSubject}" and ready for next task`
24
+ : '💤 Available and waiting for assignment',
25
+ type: 'idle',
26
+ icon: 'idle'
27
+ };
28
+
29
+ case 'task_completed':
30
+ return {
31
+ text: `✅ Completed: ${parsed.taskSubject || 'Task'}`,
32
+ type: 'success',
33
+ icon: 'check'
34
+ };
35
+
36
+ case 'task_assigned':
37
+ return {
38
+ text: `📋 Working on: ${parsed.taskSubject || 'New task'}`,
39
+ type: 'working',
40
+ icon: 'zap'
41
+ };
42
+
43
+ case 'error':
44
+ return {
45
+ text: `⚠️ Issue: ${parsed.message || 'Something went wrong'}`,
46
+ type: 'error',
47
+ icon: 'alert'
48
+ };
49
+
50
+ case 'status_update':
51
+ return {
52
+ text: `📊 ${parsed.message || 'Status update'}`,
53
+ type: 'info',
54
+ icon: 'message'
55
+ };
56
+
57
+ default:
58
+ // Unknown JSON type, show content if available
59
+ if (parsed.content) {
60
+ return { text: parsed.content, type: 'message', icon: 'message' };
61
+ }
62
+ if (parsed.message) {
63
+ return { text: parsed.message, type: 'message', icon: 'message' };
64
+ }
65
+ return { text: 'Message received', type: 'message', icon: 'message' };
66
+ }
67
+ } catch (e) {
68
+ // Not JSON, return as-is but make it friendlier
69
+ if (!text || text.trim() === '') {
70
+ return { text: '👋 Said hello', type: 'message', icon: 'message' };
71
+ }
72
+
73
+ // If text is too long, show summary
74
+ if (text.length > 200) {
75
+ return {
76
+ text: text.substring(0, 150) + '...',
77
+ fullText: text,
78
+ type: 'message',
79
+ icon: 'message'
80
+ };
81
+ }
82
+
83
+ return { text, type: 'message', icon: 'message' };
84
+ }
85
+ };
86
+
87
+ const getIconComponent = (iconType) => {
88
+ switch (iconType) {
89
+ case 'check': return CheckCircle;
90
+ case 'alert': return AlertCircle;
91
+ case 'zap': return Zap;
92
+ case 'idle': return Bot;
93
+ default: return MessageSquare;
94
+ }
95
+ };
96
+
97
+ export function LiveCommunication({ teams }) {
98
+ const [messages, setMessages] = useState([]);
99
+ const [selectedTeam, setSelectedTeam] = useState(null);
100
+ const [loading, setLoading] = useState(false);
101
+ const [error, setError] = useState(null);
102
+ const [autoScroll, setAutoScroll] = useState(true);
103
+ const messagesEndRef = React.useRef(null);
104
+
105
+ useEffect(() => {
106
+ if (teams && teams.length > 0 && !selectedTeam) {
107
+ setSelectedTeam(teams[0].name);
108
+ }
109
+ }, [teams, selectedTeam]);
110
+
111
+ // Auto-scroll to bottom when new messages arrive
112
+ useEffect(() => {
113
+ if (autoScroll && messagesEndRef.current) {
114
+ messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
115
+ }
116
+ }, [messages, autoScroll]);
117
+
118
+ // Fetch real inbox messages from API
119
+ useEffect(() => {
120
+ if (!selectedTeam) return;
121
+
122
+ const fetchMessages = async () => {
123
+ try {
124
+ setLoading(true);
125
+ setError(null);
126
+
127
+ const response = await fetch(`http://localhost:3001/api/teams/${encodeURIComponent(selectedTeam)}/inboxes`);
128
+
129
+ if (!response.ok) {
130
+ throw new Error(`Failed to fetch messages: ${response.status}`);
131
+ }
132
+
133
+ const data = await response.json();
134
+
135
+ // Convert inbox data to message format
136
+ const allMessages = [];
137
+
138
+ if (data.inboxes && typeof data.inboxes === 'object') {
139
+ Object.entries(data.inboxes).forEach(([agentName, inbox]) => {
140
+ if (inbox.messages && Array.isArray(inbox.messages)) {
141
+ inbox.messages.forEach(msg => {
142
+ // Convert to natural language
143
+ const naturalMsg = parseMessageToNatural(msg.text, msg.summary);
144
+
145
+ allMessages.push({
146
+ id: `${agentName}-${msg.timestamp}-${Math.random()}`,
147
+ from: msg.from || agentName,
148
+ to: agentName,
149
+ message: naturalMsg.text,
150
+ fullText: naturalMsg.fullText || msg.text,
151
+ timestamp: new Date(msg.timestamp),
152
+ type: naturalMsg.type,
153
+ icon: naturalMsg.icon,
154
+ color: msg.color || 'blue',
155
+ read: msg.read || false
156
+ });
157
+ });
158
+ }
159
+ });
160
+ }
161
+
162
+ // Sort by timestamp (oldest first for chat-style)
163
+ allMessages.sort((a, b) => a.timestamp - b.timestamp);
164
+
165
+ // Keep last 50 messages
166
+ setMessages(allMessages.slice(-50));
167
+ setLoading(false);
168
+ } catch (err) {
169
+ console.error('Error fetching messages:', err);
170
+ setError(err.message);
171
+ setLoading(false);
172
+ }
173
+ };
174
+
175
+ // Fetch immediately
176
+ fetchMessages();
177
+
178
+ // Then poll every 5 seconds
179
+ const interval = setInterval(fetchMessages, 5000);
180
+
181
+ return () => clearInterval(interval);
182
+ }, [selectedTeam]);
183
+
184
+ const currentTeam = teams?.find(t => t.name === selectedTeam);
185
+
186
+ return (
187
+ <div className="card" style={{ height: '500px', display: 'flex', flexDirection: 'column' }}>
188
+ <div className="flex items-center justify-between mb-4">
189
+ <div className="flex items-center gap-2">
190
+ <MessageSquare className="h-5 w-5 text-claude-orange" />
191
+ <h3 className="text-lg font-semibold text-white">Live Communication</h3>
192
+ </div>
193
+ <div className="flex items-center gap-3">
194
+ <button
195
+ onClick={() => setAutoScroll(!autoScroll)}
196
+ className={`text-xs px-2 py-1 rounded transition-colors ${
197
+ autoScroll
198
+ ? 'bg-green-500/20 text-green-400'
199
+ : 'bg-gray-700 text-gray-400'
200
+ }`}
201
+ title={autoScroll ? 'Auto-scroll enabled' : 'Auto-scroll disabled'}
202
+ >
203
+ {autoScroll ? '📍 Auto-scroll' : '🔒 Scroll locked'}
204
+ </button>
205
+ <span className="text-xs text-green-400 flex items-center gap-1">
206
+ <span className="h-2 w-2 rounded-full bg-green-400 animate-pulse"></span>
207
+ LIVE
208
+ </span>
209
+ </div>
210
+ </div>
211
+
212
+ {/* Team Selector */}
213
+ {teams && teams.length > 0 && (
214
+ <div className="mb-4">
215
+ <select
216
+ value={selectedTeam || ''}
217
+ onChange={(e) => setSelectedTeam(e.target.value)}
218
+ className="w-full px-3 py-2 bg-gray-700 text-white rounded-lg border border-gray-600 focus:border-claude-orange focus:outline-none"
219
+ >
220
+ {teams.map(team => (
221
+ <option key={team.name} value={team.name}>
222
+ {team.name} ({team.config?.members?.length || 0} members)
223
+ </option>
224
+ ))}
225
+ </select>
226
+ </div>
227
+ )}
228
+
229
+ {/* Messages Area */}
230
+ <div className="flex-1 overflow-y-auto space-y-2 mb-4" style={{ minHeight: 0 }}>
231
+ {error ? (
232
+ <div className="text-center py-8 text-red-400">
233
+ <MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
234
+ <p className="text-sm">Error loading messages</p>
235
+ <p className="text-xs mt-1">{error}</p>
236
+ </div>
237
+ ) : loading && messages.length === 0 ? (
238
+ <div className="text-center py-8 text-gray-400">
239
+ <MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50 animate-pulse" />
240
+ <p className="text-sm">Loading messages...</p>
241
+ </div>
242
+ ) : messages.length === 0 ? (
243
+ <div className="text-center py-8 text-gray-400">
244
+ <MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
245
+ <p className="text-sm">No messages yet</p>
246
+ <p className="text-xs mt-1">Agent communication will appear here</p>
247
+ </div>
248
+ ) : (
249
+ messages.map(msg => {
250
+ const Icon = getIconComponent(msg.icon);
251
+ const typeColors = {
252
+ idle: 'bg-gray-700/30 border-gray-600/50',
253
+ success: 'bg-green-900/20 border-green-700/50',
254
+ error: 'bg-red-900/20 border-red-700/50',
255
+ working: 'bg-blue-900/20 border-blue-700/50',
256
+ message: 'bg-gray-700/50 border-gray-600 hover:border-claude-orange'
257
+ };
258
+
259
+ return (
260
+ <div
261
+ key={msg.id}
262
+ className={`p-3 rounded-lg border transition-all hover:shadow-lg ${
263
+ typeColors[msg.type] || typeColors.message
264
+ }`}
265
+ >
266
+ <div className="flex items-start gap-3">
267
+ {/* Icon */}
268
+ <div className={`mt-0.5 ${
269
+ msg.type === 'success' ? 'text-green-400' :
270
+ msg.type === 'error' ? 'text-red-400' :
271
+ msg.type === 'working' ? 'text-blue-400' :
272
+ msg.type === 'idle' ? 'text-gray-400' :
273
+ 'text-claude-orange'
274
+ }`}>
275
+ <Icon className="h-4 w-4" />
276
+ </div>
277
+
278
+ {/* Content */}
279
+ <div className="flex-1 min-w-0">
280
+ <div className="flex items-start justify-between mb-1">
281
+ <div className="flex items-center gap-2 flex-wrap">
282
+ <span className="text-sm font-semibold text-white">{msg.from}</span>
283
+ {msg.to && msg.from !== msg.to && (
284
+ <>
285
+ <span className="text-xs text-gray-500">→</span>
286
+ <span className="text-xs text-gray-400">{msg.to}</span>
287
+ </>
288
+ )}
289
+ {!msg.read && (
290
+ <span className="text-xs bg-blue-500/30 text-blue-300 px-2 py-0.5 rounded-full">New</span>
291
+ )}
292
+ </div>
293
+ <span className="text-xs text-gray-400 whitespace-nowrap ml-2">
294
+ {dayjs(msg.timestamp).fromNow()}
295
+ </span>
296
+ </div>
297
+ <p className="text-sm text-gray-200 leading-relaxed">{msg.message}</p>
298
+ </div>
299
+ </div>
300
+ </div>
301
+ );
302
+ })
303
+ )}
304
+ <div ref={messagesEndRef} />
305
+ </div>
306
+
307
+ {/* Team Info */}
308
+ {currentTeam && (
309
+ <div className="pt-4 border-t border-gray-700">
310
+ <div className="flex items-center justify-between text-sm">
311
+ <div className="flex items-center gap-2 text-gray-400">
312
+ <Users className="h-4 w-4" />
313
+ <span>{currentTeam.config?.members?.length || 0} members online</span>
314
+ </div>
315
+ <div className="flex items-center gap-2 text-gray-400">
316
+ <Clock className="h-4 w-4" />
317
+ <span>
318
+ {currentTeam.tasks?.filter(t => t.status === 'in_progress').length || 0} active
319
+ </span>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ )}
324
+ </div>
325
+ );
326
+ }
@@ -0,0 +1,100 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Zap } from 'lucide-react';
4
+
5
+ export function LiveMetrics({ stats }) {
6
+ const [pulse, setPulse] = useState(false);
7
+
8
+ useEffect(() => {
9
+ if (stats) {
10
+ setPulse(true);
11
+ const timer = setTimeout(() => setPulse(false), 300);
12
+ return () => clearTimeout(timer);
13
+ }
14
+ }, [stats]);
15
+
16
+ if (!stats) return null;
17
+
18
+ const taskCompletionRate = stats.totalTasks > 0
19
+ ? Math.round((stats.completedTasks / stats.totalTasks) * 100)
20
+ : 0;
21
+
22
+ const activeTasksRate = stats.totalTasks > 0
23
+ ? Math.round((stats.inProgressTasks / stats.totalTasks) * 100)
24
+ : 0;
25
+
26
+ return (
27
+ <div className="card" style={{ background: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)' }}>
28
+ <div className="flex items-center gap-2 mb-4">
29
+ <div className={`p-2 rounded-lg bg-claude-orange ${pulse ? 'animate-pulse' : ''}`} style={{ background: '#f97316' }}>
30
+ <Zap className="h-5 w-5 text-white" />
31
+ </div>
32
+ <h3 className="text-lg font-semibold text-white">Live Metrics</h3>
33
+ <span className="ml-auto text-xs text-green-400 flex items-center gap-1">
34
+ <span className="h-2 w-2 rounded-full bg-green-400" style={{ animation: 'pulse 2s infinite' }}></span>
35
+ LIVE
36
+ </span>
37
+ </div>
38
+
39
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
40
+ <div>
41
+ <div className="flex items-center justify-between mb-2">
42
+ <span className="text-sm text-gray-400">Task Completion</span>
43
+ <span className="text-sm font-semibold text-white">{taskCompletionRate}%</span>
44
+ </div>
45
+ <div className="w-full bg-gray-700 rounded-full h-2">
46
+ <div
47
+ className="h-2 rounded-full transition-all duration-500"
48
+ style={{
49
+ width: `${taskCompletionRate}%`,
50
+ background: 'linear-gradient(90deg, #10b981 0%, #34d399 100%)'
51
+ }}
52
+ ></div>
53
+ </div>
54
+ </div>
55
+
56
+ <div>
57
+ <div className="flex items-center justify-between mb-2">
58
+ <span className="text-sm text-gray-400">Active Tasks</span>
59
+ <span className="text-sm font-semibold text-white">{activeTasksRate}%</span>
60
+ </div>
61
+ <div className="w-full bg-gray-700 rounded-full h-2">
62
+ <div
63
+ className="h-2 rounded-full transition-all duration-500"
64
+ style={{
65
+ width: `${activeTasksRate}%`,
66
+ background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%)'
67
+ }}
68
+ ></div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <div className="grid grid-cols-3 gap-4 mt-4 pt-4" style={{ borderTop: '1px solid #374151' }}>
74
+ <div className="text-center">
75
+ <div className="text-2xl font-bold text-white">{stats.totalAgents}</div>
76
+ <div className="text-xs text-gray-400">Active Agents</div>
77
+ </div>
78
+ <div className="text-center">
79
+ <div className="text-2xl font-bold text-blue-400">{stats.inProgressTasks}</div>
80
+ <div className="text-xs text-gray-400">Working Now</div>
81
+ </div>
82
+ <div className="text-center">
83
+ <div className="text-2xl font-bold text-green-400">{stats.completedTasks}</div>
84
+ <div className="text-xs text-gray-400">Completed</div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ LiveMetrics.propTypes = {
92
+ stats: PropTypes.shape({
93
+ totalTeams: PropTypes.number,
94
+ totalAgents: PropTypes.number,
95
+ totalTasks: PropTypes.number,
96
+ inProgressTasks: PropTypes.number,
97
+ completedTasks: PropTypes.number,
98
+ blockedTasks: PropTypes.number
99
+ })
100
+ };