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,298 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { MessageCircle, ArrowRight, Radio } from 'lucide-react';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
6
|
+
dayjs.extend(relativeTime);
|
|
7
|
+
|
|
8
|
+
// Convert technical messages to natural language
|
|
9
|
+
const parseMessageToNatural = (text, summary) => {
|
|
10
|
+
// If there's a clear summary, use it
|
|
11
|
+
if (summary && !summary.includes('{') && !summary.includes('idle_notification')) {
|
|
12
|
+
// Determine type from summary content
|
|
13
|
+
let type = 'status';
|
|
14
|
+
if (summary.toLowerCase().includes('completed') || summary.includes('✓') || summary.includes('✅')) {
|
|
15
|
+
type = 'completion';
|
|
16
|
+
} else if (summary.toLowerCase().includes('question') || summary.includes('?')) {
|
|
17
|
+
type = 'question';
|
|
18
|
+
} else if (summary.toLowerCase().includes('coordin') || summary.toLowerCase().includes('discuss') || summary.toLowerCase().includes('help')) {
|
|
19
|
+
type = 'coordination';
|
|
20
|
+
}
|
|
21
|
+
return { text: summary, type };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Try to parse as JSON
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(text);
|
|
27
|
+
|
|
28
|
+
switch (parsed.type) {
|
|
29
|
+
case 'idle_notification':
|
|
30
|
+
return {
|
|
31
|
+
text: parsed.lastTaskSubject
|
|
32
|
+
? `💤 Finished "${parsed.lastTaskSubject}" - ready for next task`
|
|
33
|
+
: '💤 Available and waiting for assignment',
|
|
34
|
+
type: 'status'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
case 'task_completed':
|
|
38
|
+
return {
|
|
39
|
+
text: `✅ Completed: ${parsed.taskSubject || 'Task'}`,
|
|
40
|
+
type: 'completion'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
case 'task_assigned':
|
|
44
|
+
return {
|
|
45
|
+
text: `📋 Started working on: ${parsed.taskSubject || 'New task'}`,
|
|
46
|
+
type: 'status'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
case 'question':
|
|
50
|
+
return {
|
|
51
|
+
text: `❓ ${parsed.message || parsed.content || 'Question raised'}`,
|
|
52
|
+
type: 'question'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
case 'coordination':
|
|
56
|
+
return {
|
|
57
|
+
text: `🤝 ${parsed.message || parsed.content || 'Coordinating with team'}`,
|
|
58
|
+
type: 'coordination'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
return {
|
|
63
|
+
text: parsed.message || parsed.content || 'Message received',
|
|
64
|
+
type: 'status'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// Not JSON, use as-is
|
|
69
|
+
if (!text || text.trim() === '') {
|
|
70
|
+
return { text: '👋 Said hello', type: 'status' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Truncate if too long
|
|
74
|
+
if (text.length > 200) {
|
|
75
|
+
return {
|
|
76
|
+
text: text.substring(0, 150) + '...',
|
|
77
|
+
type: 'status'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { text, type: 'status' };
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function RealTimeMessages({ teams }) {
|
|
86
|
+
const [messages, setMessages] = useState([]);
|
|
87
|
+
const [filter, setFilter] = useState('all');
|
|
88
|
+
const [loading, setLoading] = useState(false);
|
|
89
|
+
const [error, setError] = useState(null);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
// Fetch real inbox messages from all teams
|
|
93
|
+
if (!teams || teams.length === 0) return;
|
|
94
|
+
|
|
95
|
+
const fetchAllMessages = async () => {
|
|
96
|
+
try {
|
|
97
|
+
setLoading(true);
|
|
98
|
+
setError(null);
|
|
99
|
+
|
|
100
|
+
const allMessages = [];
|
|
101
|
+
|
|
102
|
+
// Fetch messages from each team
|
|
103
|
+
for (const team of teams) {
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(`http://localhost:3001/api/teams/${encodeURIComponent(team.name)}/inboxes`);
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
console.warn(`Failed to fetch messages for team ${team.name}: ${response.status}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = await response.json();
|
|
113
|
+
|
|
114
|
+
// Convert inbox data to message format
|
|
115
|
+
if (data.inboxes && typeof data.inboxes === 'object') {
|
|
116
|
+
Object.entries(data.inboxes).forEach(([agentName, inbox]) => {
|
|
117
|
+
if (inbox.messages && Array.isArray(inbox.messages)) {
|
|
118
|
+
inbox.messages.forEach(msg => {
|
|
119
|
+
// Convert to natural language
|
|
120
|
+
const naturalMsg = parseMessageToNatural(msg.text, msg.summary);
|
|
121
|
+
|
|
122
|
+
allMessages.push({
|
|
123
|
+
id: `${team.name}-${agentName}-${msg.timestamp}-${Math.random()}`,
|
|
124
|
+
from: msg.from || agentName,
|
|
125
|
+
to: agentName,
|
|
126
|
+
team: team.name,
|
|
127
|
+
type: naturalMsg.type,
|
|
128
|
+
message: naturalMsg.text,
|
|
129
|
+
timestamp: new Date(msg.timestamp),
|
|
130
|
+
color: msg.color || 'blue',
|
|
131
|
+
read: msg.read || false
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(`Error fetching messages for team ${team.name}:`, err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Sort by timestamp (newest first)
|
|
143
|
+
allMessages.sort((a, b) => b.timestamp - a.timestamp);
|
|
144
|
+
|
|
145
|
+
// Keep last 100 messages
|
|
146
|
+
setMessages(allMessages.slice(0, 100));
|
|
147
|
+
setLoading(false);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error('Error fetching messages:', err);
|
|
150
|
+
setError(err.message);
|
|
151
|
+
setLoading(false);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Fetch immediately
|
|
156
|
+
fetchAllMessages();
|
|
157
|
+
|
|
158
|
+
// Then poll every 5 seconds
|
|
159
|
+
const interval = setInterval(fetchAllMessages, 5000);
|
|
160
|
+
|
|
161
|
+
return () => clearInterval(interval);
|
|
162
|
+
}, [teams]);
|
|
163
|
+
|
|
164
|
+
const filteredMessages = filter === 'all'
|
|
165
|
+
? messages
|
|
166
|
+
: messages.filter(m => m.type === filter);
|
|
167
|
+
|
|
168
|
+
const getMessageColor = (type) => {
|
|
169
|
+
switch (type) {
|
|
170
|
+
case 'status': return 'message-status';
|
|
171
|
+
case 'completion': return 'message-completion';
|
|
172
|
+
case 'coordination': return 'message-coordination';
|
|
173
|
+
case 'question': return 'message-question';
|
|
174
|
+
default: return 'border-gray-600 bg-gray-700/30';
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const getTypeIcon = (type) => {
|
|
179
|
+
switch (type) {
|
|
180
|
+
case 'status': return '📊';
|
|
181
|
+
case 'completion': return '✅';
|
|
182
|
+
case 'coordination': return '🤝';
|
|
183
|
+
case 'question': return '❓';
|
|
184
|
+
default: return '💬';
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className="card" style={{ height: '600px', display: 'flex', flexDirection: 'column' }}>
|
|
190
|
+
<div className="flex items-center justify-between mb-4">
|
|
191
|
+
<div className="flex items-center gap-2">
|
|
192
|
+
<Radio className="h-5 w-5 text-claude-orange" />
|
|
193
|
+
<h3 className="text-lg font-semibold text-white">Agent Inter-Communication</h3>
|
|
194
|
+
</div>
|
|
195
|
+
<span className="live-stats-indicator">
|
|
196
|
+
<span className="h-2 w-2 rounded-full bg-green-400 animate-pulse"></span>
|
|
197
|
+
{messages.length} {messages.length === 1 ? 'message' : 'messages'}
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Filter Tabs */}
|
|
202
|
+
<div className="flex gap-2 mb-4 overflow-x-auto pb-1">
|
|
203
|
+
{['all', 'status', 'completion', 'coordination', 'question'].map(f => (
|
|
204
|
+
<button
|
|
205
|
+
key={f}
|
|
206
|
+
onClick={() => setFilter(f)}
|
|
207
|
+
className={`filter-button whitespace-nowrap ${
|
|
208
|
+
filter === f ? 'filter-button-active' : 'filter-button-inactive'
|
|
209
|
+
}`}
|
|
210
|
+
>
|
|
211
|
+
{f.charAt(0).toUpperCase() + f.slice(1)}
|
|
212
|
+
</button>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Messages Stream */}
|
|
217
|
+
<div className="flex-1 overflow-y-auto space-y-2" style={{ minHeight: 0 }}>
|
|
218
|
+
{error ? (
|
|
219
|
+
<div className="text-center py-12 text-red-400">
|
|
220
|
+
<MessageCircle className="h-16 w-16 mx-auto mb-3 opacity-50" />
|
|
221
|
+
<p className="text-sm">Error loading messages</p>
|
|
222
|
+
<p className="text-xs mt-1">{error}</p>
|
|
223
|
+
</div>
|
|
224
|
+
) : loading && filteredMessages.length === 0 ? (
|
|
225
|
+
<div className="text-center py-12 text-gray-400">
|
|
226
|
+
<MessageCircle className="h-16 w-16 mx-auto mb-3 opacity-50 animate-pulse" />
|
|
227
|
+
<p className="text-sm">Loading messages...</p>
|
|
228
|
+
</div>
|
|
229
|
+
) : filteredMessages.length === 0 ? (
|
|
230
|
+
<div className="text-center py-12 text-gray-400">
|
|
231
|
+
<MessageCircle className="h-16 w-16 mx-auto mb-3 opacity-50" />
|
|
232
|
+
<p className="text-sm">No messages yet</p>
|
|
233
|
+
<p className="text-xs mt-1">Agent communication will stream here in real-time</p>
|
|
234
|
+
</div>
|
|
235
|
+
) : (
|
|
236
|
+
filteredMessages.map(msg => (
|
|
237
|
+
<div
|
|
238
|
+
key={msg.id}
|
|
239
|
+
className={`message-card p-3.5 rounded-xl border transition-all ${getMessageColor(msg.type)}`}
|
|
240
|
+
style={{ animation: 'fadeIn 0.3s ease-out' }}
|
|
241
|
+
>
|
|
242
|
+
<div className="flex items-start gap-3">
|
|
243
|
+
<span className="message-emoji text-2xl">{getTypeIcon(msg.type)}</span>
|
|
244
|
+
<div className="flex-1 min-w-0">
|
|
245
|
+
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
|
|
246
|
+
<span className="agent-badge">{msg.from}</span>
|
|
247
|
+
<ArrowRight className="message-arrow h-3.5 w-3.5" />
|
|
248
|
+
<span className="text-sm text-gray-300 font-medium">{msg.to}</span>
|
|
249
|
+
<span className="message-timestamp ml-auto">
|
|
250
|
+
{dayjs(msg.timestamp).fromNow()}
|
|
251
|
+
</span>
|
|
252
|
+
</div>
|
|
253
|
+
<p className="message-text">{msg.message}</p>
|
|
254
|
+
{msg.team && (
|
|
255
|
+
<div className="mt-2 flex items-center gap-2">
|
|
256
|
+
<span className="text-xs text-gray-400 font-medium">Team: {msg.team}</span>
|
|
257
|
+
{!msg.read && (
|
|
258
|
+
<span className="unread-badge">New</span>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
))
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Stats Footer */}
|
|
270
|
+
<div className="pt-4 mt-4 border-t border-gray-700">
|
|
271
|
+
<div className="grid grid-cols-2 gap-4 text-center">
|
|
272
|
+
<div>
|
|
273
|
+
<div className="text-xl font-bold text-blue-400">
|
|
274
|
+
{messages.filter(m => m.type === 'status').length}
|
|
275
|
+
</div>
|
|
276
|
+
<div className="text-xs text-gray-400">Status Updates</div>
|
|
277
|
+
</div>
|
|
278
|
+
<div>
|
|
279
|
+
<div className="text-xl font-bold text-green-400">
|
|
280
|
+
{messages.filter(m => m.type === 'completion').length}
|
|
281
|
+
</div>
|
|
282
|
+
<div className="text-xs text-gray-400">Completions</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
RealTimeMessages.propTypes = {
|
|
291
|
+
teams: PropTypes.arrayOf(
|
|
292
|
+
PropTypes.shape({
|
|
293
|
+
name: PropTypes.string,
|
|
294
|
+
config: PropTypes.object,
|
|
295
|
+
tasks: PropTypes.array
|
|
296
|
+
})
|
|
297
|
+
)
|
|
298
|
+
};
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skeleton Loader Components
|
|
3
|
+
*
|
|
4
|
+
* Animated placeholder components for loading states
|
|
5
|
+
* Provides better UX than spinners by showing content structure
|
|
6
|
+
*
|
|
7
|
+
* Usage: Replace spinner/loading states with appropriate skeleton component
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* SkeletonCard - Full card skeleton
|
|
14
|
+
* Use for: TeamCard, SystemStatus, etc.
|
|
15
|
+
*/
|
|
16
|
+
export function SkeletonCard() {
|
|
17
|
+
return (
|
|
18
|
+
<div className="card p-6 space-y-4">
|
|
19
|
+
<div className="flex items-center gap-4">
|
|
20
|
+
<div className="skeleton-animated w-16 h-16 rounded-full flex-shrink-0" />
|
|
21
|
+
<div className="flex-1 space-y-2">
|
|
22
|
+
<div className="skeleton-animated h-6 w-3/4" />
|
|
23
|
+
<div className="skeleton-animated h-4 w-1/2" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div className="space-y-2">
|
|
27
|
+
<div className="skeleton-animated h-4 w-full" />
|
|
28
|
+
<div className="skeleton-animated h-4 w-5/6" />
|
|
29
|
+
<div className="skeleton-animated h-4 w-4/5" />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* SkeletonStat - Stat card skeleton
|
|
37
|
+
* Use for: StatsOverview items
|
|
38
|
+
*/
|
|
39
|
+
export function SkeletonStat() {
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex items-center gap-3">
|
|
42
|
+
<div className="skeleton-animated w-12 h-12 rounded-lg flex-shrink-0" />
|
|
43
|
+
<div className="space-y-2 flex-1">
|
|
44
|
+
<div className="skeleton-animated h-3 w-20" />
|
|
45
|
+
<div className="skeleton-animated h-6 w-12" />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* SkeletonStatsOverview - Full stats overview skeleton
|
|
53
|
+
* Use for: Main stats bar at top of dashboard
|
|
54
|
+
*/
|
|
55
|
+
export function SkeletonStatsOverview() {
|
|
56
|
+
return (
|
|
57
|
+
<div className="card p-4 mb-4">
|
|
58
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
|
59
|
+
{[...Array(6)].map((_, i) => (
|
|
60
|
+
<SkeletonStat key={i} />
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* SkeletonTaskItem - Single task item skeleton
|
|
69
|
+
* Use for: TaskList items
|
|
70
|
+
*/
|
|
71
|
+
export function SkeletonTaskItem() {
|
|
72
|
+
return (
|
|
73
|
+
<div className="bg-gray-700/30 rounded-lg p-4 border border-gray-600">
|
|
74
|
+
<div className="flex items-start gap-3">
|
|
75
|
+
<div className="skeleton-animated w-5 h-5 rounded-full mt-1 flex-shrink-0" />
|
|
76
|
+
<div className="flex-1 space-y-3">
|
|
77
|
+
<div className="flex items-start justify-between gap-2">
|
|
78
|
+
<div className="skeleton-animated h-5 w-2/3" />
|
|
79
|
+
<div className="skeleton-animated h-6 w-20 rounded-full" />
|
|
80
|
+
</div>
|
|
81
|
+
<div className="space-y-2">
|
|
82
|
+
<div className="skeleton-animated h-4 w-full" />
|
|
83
|
+
<div className="skeleton-animated h-4 w-4/5" />
|
|
84
|
+
</div>
|
|
85
|
+
<div className="flex gap-2">
|
|
86
|
+
<div className="skeleton-animated h-6 w-24 rounded" />
|
|
87
|
+
<div className="skeleton-animated h-6 w-28 rounded" />
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* SkeletonTaskList - Full task list skeleton
|
|
97
|
+
* Use for: TaskList loading state
|
|
98
|
+
*/
|
|
99
|
+
export function SkeletonTaskList({ count = 3 }) {
|
|
100
|
+
return (
|
|
101
|
+
<div className="space-y-2">
|
|
102
|
+
{[...Array(count)].map((_, i) => (
|
|
103
|
+
<SkeletonTaskItem key={i} />
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* SkeletonAgentCard - Agent card skeleton
|
|
111
|
+
* Use for: AgentCard loading state
|
|
112
|
+
*/
|
|
113
|
+
export function SkeletonAgentCard() {
|
|
114
|
+
return (
|
|
115
|
+
<div className="bg-gray-700/50 rounded-lg p-4 border border-gray-600">
|
|
116
|
+
<div className="flex items-start gap-3">
|
|
117
|
+
<div className="skeleton-animated w-10 h-10 rounded-lg flex-shrink-0" />
|
|
118
|
+
<div className="flex-1 space-y-2">
|
|
119
|
+
<div className="flex items-center gap-2">
|
|
120
|
+
<div className="skeleton-animated h-5 w-32" />
|
|
121
|
+
<div className="skeleton-animated h-5 w-12 rounded-full" />
|
|
122
|
+
</div>
|
|
123
|
+
<div className="skeleton-animated h-4 w-40" />
|
|
124
|
+
<div className="skeleton-animated h-3 w-24" />
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* SkeletonTeamCard - Team card skeleton with agents
|
|
133
|
+
* Use for: TeamCard loading state
|
|
134
|
+
*/
|
|
135
|
+
export function SkeletonTeamCard() {
|
|
136
|
+
return (
|
|
137
|
+
<div className="card space-y-6">
|
|
138
|
+
{/* Header */}
|
|
139
|
+
<div className="flex items-center justify-between">
|
|
140
|
+
<div className="flex items-center gap-3">
|
|
141
|
+
<div className="skeleton-animated w-12 h-12 rounded-lg" />
|
|
142
|
+
<div className="space-y-2">
|
|
143
|
+
<div className="skeleton-animated h-6 w-40" />
|
|
144
|
+
<div className="skeleton-animated h-4 w-32" />
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div className="skeleton-animated h-8 w-24 rounded-full" />
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Agents */}
|
|
151
|
+
<div className="space-y-3">
|
|
152
|
+
<div className="skeleton-animated h-4 w-20" />
|
|
153
|
+
<div className="space-y-2">
|
|
154
|
+
<SkeletonAgentCard />
|
|
155
|
+
<SkeletonAgentCard />
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Tasks */}
|
|
160
|
+
<div className="space-y-3">
|
|
161
|
+
<div className="skeleton-animated h-4 w-16" />
|
|
162
|
+
<SkeletonTaskList count={2} />
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* SkeletonActivityItem - Activity feed item skeleton
|
|
170
|
+
* Use for: ActivityFeed items
|
|
171
|
+
*/
|
|
172
|
+
export function SkeletonActivityItem() {
|
|
173
|
+
return (
|
|
174
|
+
<div className="flex items-start gap-3 p-3 rounded-lg bg-gray-700/30">
|
|
175
|
+
<div className="skeleton-animated w-8 h-8 rounded-full flex-shrink-0" />
|
|
176
|
+
<div className="flex-1 space-y-2">
|
|
177
|
+
<div className="skeleton-animated h-4 w-full" />
|
|
178
|
+
<div className="skeleton-animated h-3 w-3/4" />
|
|
179
|
+
<div className="skeleton-animated h-3 w-20" />
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* SkeletonActivityFeed - Full activity feed skeleton
|
|
187
|
+
* Use for: ActivityFeed loading state
|
|
188
|
+
*/
|
|
189
|
+
export function SkeletonActivityFeed({ count = 5 }) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="card">
|
|
192
|
+
<div className="flex items-center justify-between mb-4">
|
|
193
|
+
<div className="skeleton-animated h-6 w-32" />
|
|
194
|
+
<div className="skeleton-animated h-8 w-8 rounded-full" />
|
|
195
|
+
</div>
|
|
196
|
+
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
197
|
+
{[...Array(count)].map((_, i) => (
|
|
198
|
+
<SkeletonActivityItem key={i} />
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* SkeletonChart - Chart/graph skeleton
|
|
207
|
+
* Use for: LiveMetrics, charts
|
|
208
|
+
*/
|
|
209
|
+
export function SkeletonChart() {
|
|
210
|
+
return (
|
|
211
|
+
<div className="card p-6 space-y-4">
|
|
212
|
+
<div className="skeleton-animated h-6 w-48" />
|
|
213
|
+
<div className="flex items-end justify-between h-32 gap-2">
|
|
214
|
+
{[...Array(8)].map((_, i) => (
|
|
215
|
+
<div
|
|
216
|
+
key={i}
|
|
217
|
+
className="skeleton-animated flex-1"
|
|
218
|
+
style={{
|
|
219
|
+
height: `${Math.random() * 60 + 40}%`,
|
|
220
|
+
borderRadius: '4px 4px 0 0'
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
<div className="flex justify-between">
|
|
226
|
+
{[...Array(8)].map((_, i) => (
|
|
227
|
+
<div key={i} className="skeleton-animated h-3 w-8" />
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* SkeletonMetricCard - Metric card skeleton
|
|
236
|
+
* Use for: LiveMetrics cards
|
|
237
|
+
*/
|
|
238
|
+
export function SkeletonMetricCard() {
|
|
239
|
+
return (
|
|
240
|
+
<div className="stat-card p-6 space-y-4">
|
|
241
|
+
<div className="flex items-center justify-between">
|
|
242
|
+
<div className="skeleton-animated h-5 w-32" />
|
|
243
|
+
<div className="skeleton-animated h-8 w-8 rounded-lg" />
|
|
244
|
+
</div>
|
|
245
|
+
<div className="skeleton-animated h-10 w-20" />
|
|
246
|
+
<div className="skeleton-animated h-4 w-full" />
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* SkeletonTable - Table skeleton
|
|
253
|
+
* Use for: Data tables
|
|
254
|
+
*/
|
|
255
|
+
export function SkeletonTable({ rows = 5, columns = 4 }) {
|
|
256
|
+
return (
|
|
257
|
+
<div className="card overflow-hidden">
|
|
258
|
+
{/* Table header */}
|
|
259
|
+
<div className="flex gap-4 p-4 border-b border-gray-700">
|
|
260
|
+
{[...Array(columns)].map((_, i) => (
|
|
261
|
+
<div key={i} className="flex-1">
|
|
262
|
+
<div className="skeleton-animated h-4 w-3/4" />
|
|
263
|
+
</div>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
{/* Table rows */}
|
|
267
|
+
<div className="divide-y divide-gray-700">
|
|
268
|
+
{[...Array(rows)].map((_, rowIndex) => (
|
|
269
|
+
<div key={rowIndex} className="flex gap-4 p-4">
|
|
270
|
+
{[...Array(columns)].map((_, colIndex) => (
|
|
271
|
+
<div key={colIndex} className="flex-1">
|
|
272
|
+
<div className="skeleton-animated h-4 w-full" />
|
|
273
|
+
</div>
|
|
274
|
+
))}
|
|
275
|
+
</div>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* SkeletonText - Generic text skeleton
|
|
284
|
+
* Use for: Text placeholders
|
|
285
|
+
*/
|
|
286
|
+
export function SkeletonText({ lines = 3, widths = ['100%', '90%', '80%'] }) {
|
|
287
|
+
return (
|
|
288
|
+
<div className="space-y-2">
|
|
289
|
+
{[...Array(lines)].map((_, i) => (
|
|
290
|
+
<div
|
|
291
|
+
key={i}
|
|
292
|
+
className="skeleton-animated h-4"
|
|
293
|
+
style={{ width: widths[i % widths.length] }}
|
|
294
|
+
/>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* SkeletonAvatar - Avatar skeleton
|
|
302
|
+
* Use for: User avatars, agent icons
|
|
303
|
+
*/
|
|
304
|
+
export function SkeletonAvatar({ size = 'medium' }) {
|
|
305
|
+
const sizeClasses = {
|
|
306
|
+
small: 'w-8 h-8',
|
|
307
|
+
medium: 'w-12 h-12',
|
|
308
|
+
large: 'w-16 h-16',
|
|
309
|
+
xlarge: 'w-24 h-24'
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div className={`skeleton-animated rounded-full ${sizeClasses[size]}`} />
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* SkeletonGrid - Grid of skeleton items
|
|
319
|
+
* Use for: Dashboard grids
|
|
320
|
+
*/
|
|
321
|
+
export function SkeletonGrid({ columns = 3, rows = 2, Component = SkeletonCard }) {
|
|
322
|
+
const items = columns * rows;
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-${columns} gap-6`}>
|
|
326
|
+
{[...Array(items)].map((_, i) => (
|
|
327
|
+
<Component key={i} />
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* SkeletonBadge - Badge skeleton
|
|
335
|
+
* Use for: Status badges
|
|
336
|
+
*/
|
|
337
|
+
export function SkeletonBadge() {
|
|
338
|
+
return (
|
|
339
|
+
<div className="skeleton-animated h-6 w-20 rounded-full inline-block" />
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* SkeletonButton - Button skeleton
|
|
345
|
+
* Use for: Button placeholders
|
|
346
|
+
*/
|
|
347
|
+
export function SkeletonButton({ size = 'medium' }) {
|
|
348
|
+
const sizeClasses = {
|
|
349
|
+
small: 'h-8 w-20',
|
|
350
|
+
medium: 'h-10 w-24',
|
|
351
|
+
large: 'h-12 w-32'
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<div className={`skeleton-animated rounded-lg ${sizeClasses[size]}`} />
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* SkeletonDashboard - Complete dashboard skeleton
|
|
361
|
+
* Use for: Initial page load
|
|
362
|
+
*/
|
|
363
|
+
export function SkeletonDashboard() {
|
|
364
|
+
return (
|
|
365
|
+
<div className="space-y-6">
|
|
366
|
+
{/* Stats overview */}
|
|
367
|
+
<SkeletonStatsOverview />
|
|
368
|
+
|
|
369
|
+
{/* Main grid */}
|
|
370
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
371
|
+
{/* Teams column */}
|
|
372
|
+
<div className="lg:col-span-2 space-y-6">
|
|
373
|
+
<SkeletonTeamCard />
|
|
374
|
+
<SkeletonTeamCard />
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
{/* Activity feed column */}
|
|
378
|
+
<div className="lg:col-span-1">
|
|
379
|
+
<SkeletonActivityFeed />
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|