@castlekit/castle 0.1.6 → 0.3.0
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.
- package/drizzle.config.ts +7 -0
- package/next.config.ts +1 -0
- package/package.json +20 -3
- package/src/app/api/avatars/[id]/route.ts +57 -7
- package/src/app/api/openclaw/agents/status/route.ts +55 -0
- package/src/app/api/openclaw/chat/attachments/route.ts +230 -0
- package/src/app/api/openclaw/chat/channels/route.ts +214 -0
- package/src/app/api/openclaw/chat/route.ts +272 -0
- package/src/app/api/openclaw/chat/search/route.ts +149 -0
- package/src/app/api/openclaw/chat/storage/route.ts +75 -0
- package/src/app/api/openclaw/logs/route.ts +17 -3
- package/src/app/api/openclaw/restart/route.ts +6 -1
- package/src/app/api/openclaw/session/status/route.ts +42 -0
- package/src/app/api/settings/avatar/route.ts +190 -0
- package/src/app/api/settings/route.ts +88 -0
- package/src/app/chat/[channelId]/error-boundary.tsx +64 -0
- package/src/app/chat/[channelId]/page.tsx +305 -0
- package/src/app/chat/layout.tsx +96 -0
- package/src/app/chat/page.tsx +52 -0
- package/src/app/globals.css +89 -2
- package/src/app/layout.tsx +7 -1
- package/src/app/page.tsx +49 -17
- package/src/app/settings/page.tsx +300 -0
- package/src/components/chat/agent-mention-popup.tsx +89 -0
- package/src/components/chat/archived-channels.tsx +190 -0
- package/src/components/chat/channel-list.tsx +140 -0
- package/src/components/chat/chat-input.tsx +310 -0
- package/src/components/chat/create-channel-dialog.tsx +171 -0
- package/src/components/chat/markdown-content.tsx +205 -0
- package/src/components/chat/message-bubble.tsx +152 -0
- package/src/components/chat/message-list.tsx +508 -0
- package/src/components/chat/message-queue.tsx +68 -0
- package/src/components/chat/session-divider.tsx +61 -0
- package/src/components/chat/session-stats-panel.tsx +139 -0
- package/src/components/chat/storage-indicator.tsx +76 -0
- package/src/components/layout/sidebar.tsx +126 -45
- package/src/components/layout/user-menu.tsx +29 -4
- package/src/components/providers/presence-provider.tsx +8 -0
- package/src/components/providers/search-provider.tsx +81 -0
- package/src/components/search/search-dialog.tsx +269 -0
- package/src/components/ui/avatar.tsx +11 -9
- package/src/components/ui/dialog.tsx +10 -4
- package/src/components/ui/tooltip.tsx +25 -8
- package/src/components/ui/twemoji-text.tsx +37 -0
- package/src/lib/api-security.ts +125 -0
- package/src/lib/date-utils.ts +79 -0
- package/src/lib/db/__tests__/queries.test.ts +318 -0
- package/src/lib/db/index.ts +642 -0
- package/src/lib/db/queries.ts +1017 -0
- package/src/lib/db/schema.ts +160 -0
- package/src/lib/hooks/use-agent-status.ts +251 -0
- package/src/lib/hooks/use-chat.ts +775 -0
- package/src/lib/hooks/use-openclaw.ts +105 -70
- package/src/lib/hooks/use-search.ts +113 -0
- package/src/lib/hooks/use-session-stats.ts +57 -0
- package/src/lib/hooks/use-user-settings.ts +46 -0
- package/src/lib/types/chat.ts +186 -0
- package/src/lib/types/search.ts +60 -0
- package/src/middleware.ts +52 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Plus, Archive } from "lucide-react";
|
|
5
|
+
import { Sidebar } from "@/components/layout/sidebar";
|
|
6
|
+
|
|
7
|
+
import { ChannelList } from "@/components/chat/channel-list";
|
|
8
|
+
import { ArchivedChannels } from "@/components/chat/archived-channels";
|
|
9
|
+
import { CreateChannelDialog } from "@/components/chat/create-channel-dialog";
|
|
10
|
+
import { useParams, useRouter } from "next/navigation";
|
|
11
|
+
import type { Channel } from "@/lib/types/chat";
|
|
12
|
+
|
|
13
|
+
export default function ChatLayout({
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
const params = useParams();
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
const channelId = params?.channelId as string | undefined;
|
|
21
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
22
|
+
const [showArchived, setShowArchived] = useState(false);
|
|
23
|
+
const [newChannel, setNewChannel] = useState<Channel | null>(null);
|
|
24
|
+
|
|
25
|
+
const handleChannelCreated = (channel: Channel) => {
|
|
26
|
+
setNewChannel(channel);
|
|
27
|
+
setShowCreate(false);
|
|
28
|
+
router.push(`/chat/${channel.id}`);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="h-screen overflow-hidden bg-background">
|
|
33
|
+
<Sidebar variant="solid" />
|
|
34
|
+
|
|
35
|
+
<div className="h-screen ml-[80px] flex py-[20px]">
|
|
36
|
+
{/* Channel sidebar — floating glass panel, aligned with sidebar pill */}
|
|
37
|
+
<div className="w-[290px] shrink-0 px-[25px]">
|
|
38
|
+
<div className="h-full panel flex flex-col">
|
|
39
|
+
<div className="pl-4 pr-3 py-3 flex items-center justify-between shrink-0 border-b border-border">
|
|
40
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
41
|
+
Channels
|
|
42
|
+
</h2>
|
|
43
|
+
<button
|
|
44
|
+
onClick={() => setShowCreate(true)}
|
|
45
|
+
title="New channel"
|
|
46
|
+
className="flex items-center justify-center h-7 w-7 rounded-[var(--radius-sm)] bg-accent text-white hover:bg-accent/90 transition-colors cursor-pointer"
|
|
47
|
+
>
|
|
48
|
+
<Plus className="h-4 w-4 stroke-[2.5]" />
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="flex-1 overflow-y-auto px-3 py-3">
|
|
52
|
+
<ChannelList
|
|
53
|
+
activeChannelId={channelId}
|
|
54
|
+
showCreateDialog={showCreate}
|
|
55
|
+
onCreateDialogChange={setShowCreate}
|
|
56
|
+
newChannel={newChannel}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="shrink-0 px-3 pb-3">
|
|
60
|
+
<button
|
|
61
|
+
onClick={() => setShowArchived(true)}
|
|
62
|
+
className="flex items-center justify-center gap-2 w-full px-2 py-2 text-xs text-foreground-secondary hover:text-foreground transition-colors cursor-pointer rounded-[var(--radius-sm)] hover:bg-surface-hover"
|
|
63
|
+
>
|
|
64
|
+
<Archive className="h-3.5 w-3.5" />
|
|
65
|
+
Archived channels
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Main content — fills remaining space, aligned with floating boxes */}
|
|
72
|
+
<div className="flex-1 min-w-0 h-full overflow-hidden pr-[20px] flex flex-col">
|
|
73
|
+
{children}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Create channel dialog — rendered at layout root, outside glass panel */}
|
|
78
|
+
<CreateChannelDialog
|
|
79
|
+
open={showCreate}
|
|
80
|
+
onOpenChange={setShowCreate}
|
|
81
|
+
onCreated={handleChannelCreated}
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
{/* Archived channels dialog */}
|
|
85
|
+
<ArchivedChannels
|
|
86
|
+
open={showArchived}
|
|
87
|
+
onOpenChange={setShowArchived}
|
|
88
|
+
onRestored={(channel) => {
|
|
89
|
+
setNewChannel(channel);
|
|
90
|
+
setShowArchived(false);
|
|
91
|
+
router.push(`/chat/${channel.id}`);
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { MessageCircle, Loader2 } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
export default function ChatPage() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const [checking, setChecking] = useState(true);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
// Ask the DB for the last accessed channel
|
|
13
|
+
fetch("/api/openclaw/chat/channels?last=1")
|
|
14
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
15
|
+
.then((data) => {
|
|
16
|
+
if (data?.channelId) {
|
|
17
|
+
router.replace(`/chat/${data.channelId}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// No last accessed — try the most recent channel
|
|
21
|
+
return fetch("/api/openclaw/chat/channels")
|
|
22
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
23
|
+
.then((chData) => {
|
|
24
|
+
const channels = chData?.channels;
|
|
25
|
+
if (channels && channels.length > 0) {
|
|
26
|
+
router.replace(`/chat/${channels[0].id}`);
|
|
27
|
+
} else {
|
|
28
|
+
setChecking(false);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
})
|
|
32
|
+
.catch(() => setChecking(false));
|
|
33
|
+
}, [router]);
|
|
34
|
+
|
|
35
|
+
if (checking) {
|
|
36
|
+
return <div className="flex-1" />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex-1 flex items-center justify-center">
|
|
41
|
+
<div className="text-center space-y-3">
|
|
42
|
+
<MessageCircle className="h-12 w-12 mx-auto text-foreground-secondary/30" />
|
|
43
|
+
<div>
|
|
44
|
+
<h2 className="text-lg font-semibold text-foreground">Welcome to Chat</h2>
|
|
45
|
+
<p className="text-sm text-foreground-secondary mt-1">
|
|
46
|
+
Select a channel from the sidebar or create a new one to start chatting.
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
package/src/app/globals.css
CHANGED
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
--border: #35353d;
|
|
87
87
|
--border-hover: #45454d;
|
|
88
88
|
|
|
89
|
-
--foreground: #
|
|
90
|
-
--foreground-secondary: #
|
|
89
|
+
--foreground: #d1d2d3;
|
|
90
|
+
--foreground-secondary: #9a9b9e;
|
|
91
91
|
--foreground-muted: #71717a;
|
|
92
92
|
|
|
93
93
|
--accent: #3b82f6;
|
|
@@ -186,6 +186,43 @@ body {
|
|
|
186
186
|
overscroll-behavior: none;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/* Twemoji — inline emoji images matching text size exactly.
|
|
190
|
+
Sized to 1em so the img is no taller than a capital letter,
|
|
191
|
+
preventing any line-height expansion. */
|
|
192
|
+
img.emoji {
|
|
193
|
+
display: inline;
|
|
194
|
+
height: 1em;
|
|
195
|
+
width: 1em;
|
|
196
|
+
margin: 0 0.05em;
|
|
197
|
+
vertical-align: -0.1em;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Custom scrollbar — overlay style, just the thumb, no track */
|
|
201
|
+
/* WebKit (Chrome, Safari, Edge) */
|
|
202
|
+
::-webkit-scrollbar {
|
|
203
|
+
width: 6px;
|
|
204
|
+
background: none;
|
|
205
|
+
}
|
|
206
|
+
::-webkit-scrollbar-track {
|
|
207
|
+
background: none;
|
|
208
|
+
}
|
|
209
|
+
::-webkit-scrollbar-thumb {
|
|
210
|
+
background: rgba(255, 255, 255, 0.2);
|
|
211
|
+
border-radius: 3px;
|
|
212
|
+
}
|
|
213
|
+
::-webkit-scrollbar-thumb:hover {
|
|
214
|
+
background: rgba(255, 255, 255, 0.35);
|
|
215
|
+
}
|
|
216
|
+
::-webkit-scrollbar-corner {
|
|
217
|
+
background: none;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Firefox */
|
|
221
|
+
* {
|
|
222
|
+
scrollbar-width: thin;
|
|
223
|
+
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
|
224
|
+
}
|
|
225
|
+
|
|
189
226
|
/* ============================================
|
|
190
227
|
Reusable Component Patterns
|
|
191
228
|
============================================ */
|
|
@@ -221,6 +258,21 @@ body {
|
|
|
221
258
|
@apply cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50;
|
|
222
259
|
}
|
|
223
260
|
|
|
261
|
+
/* Panel — standard container style matching dashboard cards (bg-surface, bordered, rounded).
|
|
262
|
+
Use for sidebars, floating panels, content sections. */
|
|
263
|
+
.panel {
|
|
264
|
+
@apply rounded-[var(--radius-md)] bg-surface border border-border;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Selectable list — compact spacing for checkbox/radio lists, nav lists, etc.
|
|
268
|
+
Apply .selectable-list to the container; .selectable-list-item to each row. */
|
|
269
|
+
.selectable-list {
|
|
270
|
+
@apply space-y-0.5;
|
|
271
|
+
}
|
|
272
|
+
.selectable-list-item {
|
|
273
|
+
@apply flex items-center gap-2.5 px-2 py-1.5 rounded-[var(--radius-sm)] hover:bg-surface-hover cursor-pointer text-sm;
|
|
274
|
+
}
|
|
275
|
+
|
|
224
276
|
/* Input base - shared input/select/textarea styles */
|
|
225
277
|
.input-base {
|
|
226
278
|
@apply h-11 w-full rounded-[var(--radius-sm)] border px-3 py-2 text-sm text-foreground transition-all;
|
|
@@ -284,3 +336,38 @@ body {
|
|
|
284
336
|
transform: scale(1);
|
|
285
337
|
}
|
|
286
338
|
}
|
|
339
|
+
|
|
340
|
+
/* Search result highlight flash */
|
|
341
|
+
@keyframes highlight-flash {
|
|
342
|
+
0% {
|
|
343
|
+
background-color: oklch(from var(--color-accent) l c h / 0.15);
|
|
344
|
+
}
|
|
345
|
+
100% {
|
|
346
|
+
background-color: transparent;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
.animate-highlight-flash {
|
|
350
|
+
animation: highlight-flash 2s ease-out;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* Skeleton shimmer */
|
|
354
|
+
@keyframes shimmer {
|
|
355
|
+
0% {
|
|
356
|
+
background-position: -200% 0;
|
|
357
|
+
}
|
|
358
|
+
100% {
|
|
359
|
+
background-position: 200% 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
.skeleton {
|
|
363
|
+
background: linear-gradient(
|
|
364
|
+
90deg,
|
|
365
|
+
oklch(from var(--color-foreground) l c h / 0.06) 25%,
|
|
366
|
+
oklch(from var(--color-foreground) l c h / 0.12) 50%,
|
|
367
|
+
oklch(from var(--color-foreground) l c h / 0.06) 75%
|
|
368
|
+
);
|
|
369
|
+
background-size: 200% 100%;
|
|
370
|
+
animation: shimmer 1.5s ease-in-out infinite;
|
|
371
|
+
border-radius: 4px;
|
|
372
|
+
}
|
|
373
|
+
|
package/src/app/layout.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
2
|
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
3
|
import { ThemeProvider } from "next-themes";
|
|
4
|
+
import { PresenceProvider } from "@/components/providers/presence-provider";
|
|
5
|
+
import { SearchProvider } from "@/components/providers/search-provider";
|
|
4
6
|
import "./globals.css";
|
|
5
7
|
|
|
6
8
|
const geistSans = Geist({
|
|
@@ -34,7 +36,11 @@ export default function RootLayout({
|
|
|
34
36
|
enableSystem={false}
|
|
35
37
|
disableTransitionOnChange
|
|
36
38
|
>
|
|
37
|
-
|
|
39
|
+
<PresenceProvider>
|
|
40
|
+
<SearchProvider>
|
|
41
|
+
{children}
|
|
42
|
+
</SearchProvider>
|
|
43
|
+
</PresenceProvider>
|
|
38
44
|
</ThemeProvider>
|
|
39
45
|
</body>
|
|
40
46
|
</html>
|
package/src/app/page.tsx
CHANGED
|
@@ -3,18 +3,46 @@
|
|
|
3
3
|
import { useRef, useState, useCallback } from "react";
|
|
4
4
|
import { Bot, Wifi, WifiOff, Crown, RefreshCw, Loader2, AlertCircle, Camera } from "lucide-react";
|
|
5
5
|
import { Sidebar } from "@/components/layout/sidebar";
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
import { PageHeader } from "@/components/layout/page-header";
|
|
8
8
|
import { Card, CardContent } from "@/components/ui/card";
|
|
9
9
|
import { Badge } from "@/components/ui/badge";
|
|
10
10
|
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
|
11
11
|
import { cn } from "@/lib/utils";
|
|
12
12
|
import { useOpenClaw, type OpenClawAgent } from "@/lib/hooks/use-openclaw";
|
|
13
|
+
import { useAgentStatus, type AgentStatus } from "@/lib/hooks/use-agent-status";
|
|
13
14
|
|
|
14
15
|
function getInitials(name: string) {
|
|
15
16
|
return name.slice(0, 2).toUpperCase();
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function getStatusLabel(status: AgentStatus, isConnected: boolean): string {
|
|
20
|
+
if (!isConnected) return "Offline";
|
|
21
|
+
switch (status) {
|
|
22
|
+
case "thinking": return "Thinking";
|
|
23
|
+
case "active": return "Active";
|
|
24
|
+
default: return "Idle";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getStatusBadgeVariant(status: AgentStatus, isConnected: boolean): "success" | "warning" | "outline" {
|
|
29
|
+
if (!isConnected) return "outline";
|
|
30
|
+
switch (status) {
|
|
31
|
+
case "thinking": return "warning";
|
|
32
|
+
case "active": return "success";
|
|
33
|
+
default: return "outline";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getAvatarStatus(status: AgentStatus, isConnected: boolean): "online" | "offline" | "busy" | "away" {
|
|
38
|
+
if (!isConnected) return "offline";
|
|
39
|
+
switch (status) {
|
|
40
|
+
case "thinking": return "away";
|
|
41
|
+
case "active": return "online";
|
|
42
|
+
default: return "offline";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
function AgentCard({
|
|
19
47
|
agent,
|
|
20
48
|
isPrimary,
|
|
@@ -28,6 +56,8 @@ function AgentCard({
|
|
|
28
56
|
}) {
|
|
29
57
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
30
58
|
const [uploading, setUploading] = useState(false);
|
|
59
|
+
const { getStatus } = useAgentStatus();
|
|
60
|
+
const agentStatus = getStatus(agent.id);
|
|
31
61
|
|
|
32
62
|
const handleAvatarClick = useCallback(() => {
|
|
33
63
|
if (!isConnected) return;
|
|
@@ -79,23 +109,23 @@ function AgentCard({
|
|
|
79
109
|
<Card
|
|
80
110
|
variant="bordered"
|
|
81
111
|
className={cn(
|
|
82
|
-
"
|
|
112
|
+
"py-4 pl-5 pr-4 transition-colors min-h-[80px] flex items-center",
|
|
83
113
|
isConnected
|
|
84
114
|
? "hover:border-border-hover"
|
|
85
115
|
: "opacity-60"
|
|
86
116
|
)}
|
|
87
117
|
>
|
|
88
|
-
<div className="flex items-center justify-between">
|
|
118
|
+
<div className="flex items-center justify-between flex-1">
|
|
89
119
|
<div className="flex items-center gap-3">
|
|
90
120
|
{/* Clickable avatar with upload overlay */}
|
|
91
121
|
<button
|
|
92
122
|
type="button"
|
|
93
123
|
onClick={handleAvatarClick}
|
|
94
124
|
disabled={!isConnected || uploading}
|
|
95
|
-
className="relative group rounded-
|
|
125
|
+
className="relative group rounded-[4px] focus:outline-none focus-visible:ring-2 focus-visible:ring-accent leading-[0]"
|
|
96
126
|
title={isConnected ? "Click to change avatar" : undefined}
|
|
97
127
|
>
|
|
98
|
-
<Avatar size="md" status={isConnected
|
|
128
|
+
<Avatar size="md" status={getAvatarStatus(agentStatus, isConnected)} statusPulse={agentStatus === "thinking"}>
|
|
99
129
|
{agent.avatar ? (
|
|
100
130
|
<AvatarImage
|
|
101
131
|
src={agent.avatar}
|
|
@@ -109,12 +139,12 @@ function AgentCard({
|
|
|
109
139
|
)}
|
|
110
140
|
</Avatar>
|
|
111
141
|
{isConnected && !uploading && (
|
|
112
|
-
<div className="absolute inset-0 rounded-
|
|
142
|
+
<div className="absolute inset-0 rounded-[4px] bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
113
143
|
<Camera className="h-4 w-4 text-white" />
|
|
114
144
|
</div>
|
|
115
145
|
)}
|
|
116
146
|
{uploading && (
|
|
117
|
-
<div className="absolute inset-0 rounded-
|
|
147
|
+
<div className="absolute inset-0 rounded-[4px] bg-black/50 flex items-center justify-center">
|
|
118
148
|
<Loader2 className="h-4 w-4 text-white animate-spin" />
|
|
119
149
|
</div>
|
|
120
150
|
)}
|
|
@@ -146,10 +176,10 @@ function AgentCard({
|
|
|
146
176
|
</div>
|
|
147
177
|
</div>
|
|
148
178
|
<Badge
|
|
149
|
-
variant={isConnected
|
|
179
|
+
variant={getStatusBadgeVariant(agentStatus, isConnected)}
|
|
150
180
|
size="sm"
|
|
151
181
|
>
|
|
152
|
-
{isConnected
|
|
182
|
+
{getStatusLabel(agentStatus, isConnected)}
|
|
153
183
|
</Badge>
|
|
154
184
|
</div>
|
|
155
185
|
</Card>
|
|
@@ -230,13 +260,16 @@ function AgentsSkeleton() {
|
|
|
230
260
|
return (
|
|
231
261
|
<div className="grid gap-3">
|
|
232
262
|
{[1, 2, 3].map((i) => (
|
|
233
|
-
<Card key={i} variant="bordered" className="p-4">
|
|
234
|
-
<div className="flex items-center
|
|
235
|
-
<div className="
|
|
236
|
-
|
|
237
|
-
<div className="
|
|
238
|
-
|
|
263
|
+
<Card key={i} variant="bordered" className="p-4 min-h-[80px] flex items-center">
|
|
264
|
+
<div className="flex items-center justify-between flex-1">
|
|
265
|
+
<div className="flex items-center gap-3">
|
|
266
|
+
<div className="skeleton h-10 w-10 rounded-full" />
|
|
267
|
+
<div className="space-y-2">
|
|
268
|
+
<div className="skeleton h-4 w-24 rounded" />
|
|
269
|
+
<div className="skeleton h-3 w-32 rounded" />
|
|
270
|
+
</div>
|
|
239
271
|
</div>
|
|
272
|
+
<div className="skeleton h-5 w-14 rounded-full" />
|
|
240
273
|
</div>
|
|
241
274
|
</Card>
|
|
242
275
|
))}
|
|
@@ -294,7 +327,6 @@ export default function HomePage() {
|
|
|
294
327
|
return (
|
|
295
328
|
<div className="min-h-screen bg-background">
|
|
296
329
|
<Sidebar variant="solid" />
|
|
297
|
-
<UserMenu className="fixed top-5 right-6 z-50" variant="solid" />
|
|
298
330
|
|
|
299
331
|
<main className="min-h-screen ml-[80px]">
|
|
300
332
|
<div className="p-8 max-w-4xl">
|
|
@@ -331,7 +363,7 @@ export default function HomePage() {
|
|
|
331
363
|
)}
|
|
332
364
|
</div>
|
|
333
365
|
|
|
334
|
-
{agentsLoading
|
|
366
|
+
{agentsLoading || (isLoading && agents.length === 0) ? (
|
|
335
367
|
<AgentsSkeleton />
|
|
336
368
|
) : agents.length > 0 ? (
|
|
337
369
|
<div className="grid gap-3">
|