@polpo-ai/dashboard 0.1.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.
Files changed (135) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +44 -0
  3. package/app/(dashboard)/error.tsx +47 -0
  4. package/app/(dashboard)/layout.tsx +41 -0
  5. package/app/(dashboard)/loading.tsx +30 -0
  6. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
  7. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
  8. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
  9. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
  10. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
  11. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
  12. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
  13. package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
  14. package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
  15. package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
  16. package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
  17. package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
  18. package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
  19. package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
  20. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
  21. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
  22. package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
  23. package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
  24. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
  25. package/app/(dashboard)/projects/[id]/page.tsx +68 -0
  26. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
  27. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
  28. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
  29. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
  30. package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
  31. package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
  32. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
  33. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
  34. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
  35. package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
  36. package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
  37. package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
  38. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
  39. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
  40. package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
  41. package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
  42. package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
  43. package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
  44. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
  45. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
  46. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
  47. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
  48. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
  49. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
  50. package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
  51. package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
  52. package/app/(dashboard)/projects/[id]/view.tsx +421 -0
  53. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
  54. package/app/(dashboard)/projects/projects-list.tsx +126 -0
  55. package/app/(playground)/error.tsx +42 -0
  56. package/app/(playground)/layout.tsx +27 -0
  57. package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
  58. package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
  59. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
  60. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
  61. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
  62. package/app/favicon.ico +0 -0
  63. package/app/globals.css +385 -0
  64. package/app/icon.svg +6 -0
  65. package/app/layout.tsx +39 -0
  66. package/app/page.tsx +11 -0
  67. package/components/ai-elements/code-block.tsx +562 -0
  68. package/components/dashboard/agent-capabilities.tsx +326 -0
  69. package/components/dashboard/agent-identity-header.tsx +50 -0
  70. package/components/dashboard/agent-model-picker.tsx +195 -0
  71. package/components/dashboard/agent-model-selector.tsx +121 -0
  72. package/components/dashboard/agent-picker-card.tsx +46 -0
  73. package/components/dashboard/agent-studio.tsx +242 -0
  74. package/components/dashboard/agents-table.tsx +48 -0
  75. package/components/dashboard/breadcrumb.tsx +70 -0
  76. package/components/dashboard/builder-chat.tsx +418 -0
  77. package/components/dashboard/chat-shell.tsx +40 -0
  78. package/components/dashboard/client-picker.tsx +63 -0
  79. package/components/dashboard/command-snippet.tsx +77 -0
  80. package/components/dashboard/connect-dialog.tsx +680 -0
  81. package/components/dashboard/copy-button.tsx +45 -0
  82. package/components/dashboard/copy-card.tsx +48 -0
  83. package/components/dashboard/file-browser.tsx +161 -0
  84. package/components/dashboard/hint.tsx +22 -0
  85. package/components/dashboard/inference-mode.tsx +15 -0
  86. package/components/dashboard/manual-refresh-button.tsx +52 -0
  87. package/components/dashboard/markdown.tsx +31 -0
  88. package/components/dashboard/mcp-install-panel.tsx +196 -0
  89. package/components/dashboard/mission-graph.tsx +262 -0
  90. package/components/dashboard/nav-tabs.tsx +86 -0
  91. package/components/dashboard/polpo-chat.tsx +468 -0
  92. package/components/dashboard/project-copilot.tsx +253 -0
  93. package/components/dashboard/sdk-snippet-panel.tsx +194 -0
  94. package/components/dashboard/section-header.tsx +37 -0
  95. package/components/dashboard/settings-form.tsx +2477 -0
  96. package/components/dashboard/sidebar.tsx +385 -0
  97. package/components/dashboard/skeletons.tsx +1054 -0
  98. package/components/dashboard/skills-install-wizard.tsx +273 -0
  99. package/components/dashboard/swarm-runs.tsx +316 -0
  100. package/components/dashboard/tab-toggle.tsx +35 -0
  101. package/components/dashboard/task-studio.tsx +224 -0
  102. package/components/dashboard/top-header.tsx +203 -0
  103. package/components/dashboard/webhook-deliveries.tsx +234 -0
  104. package/components/icons/coding-agents.tsx +151 -0
  105. package/components/json-ld.tsx +8 -0
  106. package/components/providers.tsx +25 -0
  107. package/components/ui/badge.tsx +52 -0
  108. package/components/ui/button.tsx +60 -0
  109. package/components/ui/card.tsx +103 -0
  110. package/components/ui/chart.tsx +356 -0
  111. package/components/ui/command.tsx +196 -0
  112. package/components/ui/dialog.tsx +157 -0
  113. package/components/ui/input-group.tsx +158 -0
  114. package/components/ui/input.tsx +20 -0
  115. package/components/ui/multi-select.tsx +161 -0
  116. package/components/ui/popover.tsx +90 -0
  117. package/components/ui/select.tsx +201 -0
  118. package/components/ui/separator.tsx +25 -0
  119. package/components/ui/sheet.tsx +135 -0
  120. package/components/ui/skeleton.tsx +13 -0
  121. package/components/ui/tabs.tsx +56 -0
  122. package/components/ui/textarea.tsx +18 -0
  123. package/components/ui/tooltip.tsx +66 -0
  124. package/hooks/use-desktop-sidebar.tsx +50 -0
  125. package/hooks/use-mobile-sidebar.tsx +37 -0
  126. package/index.ts +14 -0
  127. package/lib/api.ts +194 -0
  128. package/lib/builder-context.ts +60 -0
  129. package/lib/data-client.ts +68 -0
  130. package/lib/get-query-client.ts +25 -0
  131. package/lib/polpo-client.tsx +49 -0
  132. package/lib/tool-catalog.ts +234 -0
  133. package/lib/use-event-catalog.ts +28 -0
  134. package/lib/utils.ts +6 -0
  135. package/package.json +99 -0
@@ -0,0 +1,1054 @@
1
+ import { Skeleton } from "#/components/ui/skeleton";
2
+ import { Shield } from "lucide-react";
3
+
4
+ /* ─── Helpers ─── */
5
+
6
+ function StatCardSkeleton({ label }: { label: string }) {
7
+ return (
8
+ <div className="border border-border bg-card p-4">
9
+ <div className="flex items-center gap-2 mb-2">
10
+ <Skeleton className="h-3.5 w-3.5 rounded" />
11
+ <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{label}</span>
12
+ </div>
13
+ <Skeleton className="h-7 w-12" />
14
+ </div>
15
+ );
16
+ }
17
+
18
+ function TableRowSkeleton({ cols }: { cols: number }) {
19
+ return (
20
+ <tr className="border-b border-border last:border-0">
21
+ {Array.from({ length: cols }).map((_, i) => (
22
+ <td key={i} className="px-4 py-3">
23
+ <Skeleton className="h-3.5 w-full max-w-[120px]" />
24
+ </td>
25
+ ))}
26
+ </tr>
27
+ );
28
+ }
29
+
30
+ function TableSkeleton({
31
+ headers,
32
+ rows = 4,
33
+ minW = "min-w-[600px]",
34
+ lastRight = false,
35
+ }: {
36
+ headers: string[];
37
+ rows?: number;
38
+ minW?: string;
39
+ lastRight?: boolean;
40
+ }) {
41
+ return (
42
+ <div className="mt-4 border border-border overflow-hidden overflow-x-auto">
43
+ <table className={`w-full text-sm ${minW}`}>
44
+ <thead>
45
+ <tr className="border-b border-border bg-secondary/50">
46
+ {headers.map((h, i) => (
47
+ <th
48
+ key={h}
49
+ className={`px-4 py-2.5 text-xs font-medium text-muted-foreground ${
50
+ lastRight && i === headers.length - 1 ? "text-right" : "text-left"
51
+ }`}
52
+ >
53
+ {h}
54
+ </th>
55
+ ))}
56
+ </tr>
57
+ </thead>
58
+ <tbody>
59
+ {Array.from({ length: rows }).map((_, i) => (
60
+ <TableRowSkeleton key={i} cols={headers.length} />
61
+ ))}
62
+ </tbody>
63
+ </table>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ function DetailTableSkeleton({ rows }: { rows: { label: string }[] }) {
69
+ return (
70
+ <div className="mt-3 border border-border bg-card overflow-hidden overflow-x-auto">
71
+ <table className="w-full text-sm min-w-[320px]">
72
+ <tbody>
73
+ {rows.map((row) => (
74
+ <tr key={row.label} className="border-b border-border last:border-0">
75
+ <td className="px-4 py-2.5 text-xs text-muted-foreground w-32">{row.label}</td>
76
+ <td className="px-4 py-2.5">
77
+ <Skeleton className="h-3 w-32" />
78
+ </td>
79
+ </tr>
80
+ ))}
81
+ </tbody>
82
+ </table>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ /* ─── Page Skeletons ─── */
88
+
89
+ /** Project overview: title + status badge + 4 stat cards + swarm panel */
90
+ export function ProjectOverviewSkeleton() {
91
+ return (
92
+ <div>
93
+ {/* Title + Ready/Running badge — matches view.tsx page header */}
94
+ <div className="mb-6 flex items-center gap-3">
95
+ <Skeleton className="h-5 w-40" />
96
+ <Skeleton className="h-5 w-16 rounded-full" />
97
+ </div>
98
+
99
+ <div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
100
+ <StatCardSkeleton label="Agents" />
101
+ <StatCardSkeleton label="Tasks" />
102
+ <StatCardSkeleton label="Missions" />
103
+ <StatCardSkeleton label="Sessions" />
104
+ </div>
105
+
106
+ {/* Swarm panel — status bar + recent runs list. Mirrors the real
107
+ layout in view.tsx so the transition skeleton → live data has
108
+ zero layout shift. */}
109
+ <div className="mt-8 flex flex-col gap-4">
110
+ <Skeleton className="h-11 w-full" />
111
+
112
+ <div className="border border-border bg-card">
113
+ <div className="flex items-center justify-between border-b border-border px-4 py-2.5">
114
+ <Skeleton className="h-3 w-24" />
115
+ <Skeleton className="h-3 w-16" />
116
+ </div>
117
+ {Array.from({ length: 5 }).map((_, i) => (
118
+ <div
119
+ key={i}
120
+ className="flex items-center gap-3 border-b border-border px-3 py-2 last:border-0"
121
+ >
122
+ <Skeleton className="size-2 rounded-full" />
123
+ <Skeleton className="h-3 w-[68px]" />
124
+ <span className="flex items-center gap-1.5">
125
+ <Skeleton className="size-4" />
126
+ <Skeleton className="h-3 w-[88px]" />
127
+ </span>
128
+ <Skeleton className="h-3 flex-1" />
129
+ <Skeleton className="hidden h-3 w-[18%] md:inline-block" />
130
+ <Skeleton className="h-3 w-12" />
131
+ </div>
132
+ ))}
133
+ </div>
134
+ </div>
135
+ </div>
136
+ );
137
+ }
138
+
139
+ /** Agents list: heading + flex list matching AgentsTable layout */
140
+ export function AgentsListSkeleton() {
141
+ return (
142
+ <div>
143
+ {/* Header: "Agents" h2 + description */}
144
+ <div>
145
+ <h2 className="text-lg font-semibold tracking-tight">Agents</h2>
146
+ <Skeleton className="mt-1 h-3 w-48" />
147
+ </div>
148
+
149
+ {/* Toolbar: search + tabs toggle + refresh */}
150
+ <div className="mt-3 flex items-center gap-2 flex-wrap">
151
+ <Skeleton className="h-7 w-48" />
152
+ <Skeleton className="h-8 w-48 rounded-lg" />
153
+ <Skeleton className="ml-auto h-3 w-16" />
154
+ </div>
155
+
156
+ {/* Table: Agent / Team / Model / Tools */}
157
+ <div className="mt-4 border border-border overflow-hidden overflow-x-auto">
158
+ <div className="min-w-[600px]">
159
+ <div className="flex items-center border-b border-border bg-secondary/50 text-left text-xs font-medium text-muted-foreground">
160
+ <span className="px-4 py-2.5 flex-1">Agent</span>
161
+ <span className="px-4 py-2.5 flex-1">Team</span>
162
+ <span className="px-4 py-2.5 flex-1">Model</span>
163
+ <span className="px-4 py-2.5 w-20">Tools</span>
164
+ <span className="w-8" />
165
+ </div>
166
+ {Array.from({ length: 3 }).map((_, i) => (
167
+ <div key={i} className="flex items-center border-b border-border last:border-0">
168
+ <span className="px-4 py-3 flex-1 space-y-1">
169
+ <Skeleton className="h-3.5 w-24" />
170
+ <Skeleton className="h-2.5 w-32" />
171
+ </span>
172
+ <span className="px-4 py-3 flex-1">
173
+ <Skeleton className="h-4 w-14 rounded" />
174
+ </span>
175
+ <span className="px-4 py-3 flex-1">
176
+ <Skeleton className="h-3 w-32" />
177
+ </span>
178
+ <span className="px-4 py-3 w-20">
179
+ <Skeleton className="h-3 w-6" />
180
+ </span>
181
+ <span className="px-2 py-3 w-8">
182
+ <Skeleton className="h-3.5 w-3.5 rounded" />
183
+ </span>
184
+ </div>
185
+ ))}
186
+ </div>
187
+ </div>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ /** Section heading placeholder — title line + description line. Mirrors
193
+ * the real <SectionHeader> so the swap has no layout shift. */
194
+ function SectionHeaderSkeleton({ descW = "w-80" }: { descW?: string }) {
195
+ return (
196
+ <div>
197
+ <Skeleton className="h-4 w-28" />
198
+ <Skeleton className={`mt-1.5 h-3 ${descW}`} />
199
+ </div>
200
+ );
201
+ }
202
+
203
+ /** Agent capabilities: section header + 4 capability card rows, each with
204
+ * a header (monogram + name + status) and a full-width model area. Matches
205
+ * <AgentCapabilities>. */
206
+ export function AgentProfileSkeleton() {
207
+ return (
208
+ <div>
209
+ <SectionHeaderSkeleton />
210
+ <div className="mt-4 flex flex-col gap-3">
211
+ {Array.from({ length: 4 }).map((_, i) => {
212
+ // Text + Video are single-slot, Image + Audio are two-slot.
213
+ const slots = i === 0 || i === 3 ? 1 : 2;
214
+ return (
215
+ <div key={i} className="border border-border bg-card p-4">
216
+ <div className="flex items-start justify-between gap-4">
217
+ <div className="flex items-center gap-2.5">
218
+ <Skeleton className="h-4 w-4" />
219
+ <Skeleton className="h-3.5 w-16" />
220
+ <Skeleton className="h-3 w-20" />
221
+ </div>
222
+ <Skeleton className="hidden h-3 w-48 sm:block" />
223
+ </div>
224
+ <div className="mt-4 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
225
+ {Array.from({ length: slots }).map((_, j) => (
226
+ <div key={j} className={slots === 1 ? "sm:col-span-2" : ""}>
227
+ <Skeleton className="mb-1.5 h-2.5 w-20" />
228
+ <Skeleton className="h-[68px] w-full" />
229
+ </div>
230
+ ))}
231
+ </div>
232
+ </div>
233
+ );
234
+ })}
235
+ </div>
236
+ </div>
237
+ );
238
+ }
239
+
240
+ /** Agent tools: section header + accordion bars (one per tool category).
241
+ * Matches AgentToolsView's <details> list. */
242
+ export function AgentToolsSkeleton() {
243
+ return (
244
+ <div>
245
+ <SectionHeaderSkeleton descW="w-72" />
246
+ <div className="mt-4 flex flex-col gap-2">
247
+ {Array.from({ length: 6 }).map((_, i) => (
248
+ <div key={i} className="border border-border bg-card">
249
+ <div className="flex items-center gap-3 px-3 py-2.5">
250
+ <Skeleton className="h-3.5 w-3.5" />
251
+ <Skeleton className="h-4 w-4" />
252
+ <Skeleton className="h-3.5 w-20" />
253
+ <Skeleton className="h-3 w-8" />
254
+ </div>
255
+ </div>
256
+ ))}
257
+ </div>
258
+ </div>
259
+ );
260
+ }
261
+
262
+ /** Agent skills: section header + card grid. Matches AgentSkillsView. */
263
+ export function AgentSkillsSkeleton() {
264
+ return (
265
+ <div>
266
+ <SectionHeaderSkeleton descW="w-80" />
267
+ <div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
268
+ {Array.from({ length: 6 }).map((_, i) => (
269
+ <div key={i} className="flex items-center gap-2.5 border border-border bg-card p-3">
270
+ <Skeleton className="size-7" />
271
+ <Skeleton className="h-3.5 w-28" />
272
+ </div>
273
+ ))}
274
+ </div>
275
+ </div>
276
+ );
277
+ }
278
+
279
+ /** Agent identity: avatar + profile + persona + responsibilities + socials */
280
+ export function AgentIdentitySkeleton() {
281
+ const sectionBlock = (rows: number) => (
282
+ <div className="border border-border bg-card px-4">
283
+ {Array.from({ length: rows }).map((_, i) => (
284
+ <div key={i} className="flex items-baseline gap-6 border-b border-border py-3 last:border-0">
285
+ <Skeleton className="h-3 w-24" />
286
+ <Skeleton className="h-3.5 w-40" />
287
+ </div>
288
+ ))}
289
+ </div>
290
+ );
291
+
292
+ return (
293
+ <div>
294
+ {/* Avatar row */}
295
+ <div className="flex items-center gap-4">
296
+ <Skeleton className="h-16 w-16 rounded-full" />
297
+ <div className="space-y-1.5">
298
+ <Skeleton className="h-3 w-14" />
299
+ <Skeleton className="h-3.5 w-48" />
300
+ </div>
301
+ </div>
302
+
303
+ <div className="mt-8">
304
+ <h3 className="text-sm font-semibold mb-2">Profile</h3>
305
+ {sectionBlock(5)}
306
+ </div>
307
+
308
+ <div className="mt-8">
309
+ <h3 className="text-sm font-semibold mb-2">Persona</h3>
310
+ {sectionBlock(3)}
311
+ </div>
312
+
313
+ <div className="mt-8">
314
+ <h3 className="text-sm font-semibold mb-2">Responsibilities</h3>
315
+ {sectionBlock(2)}
316
+ </div>
317
+
318
+ <div className="mt-8">
319
+ <h3 className="text-sm font-semibold mb-2">Socials</h3>
320
+ {sectionBlock(2)}
321
+ </div>
322
+ </div>
323
+ );
324
+ }
325
+
326
+ /** Agent system prompt: mono-space block */
327
+ export function AgentPromptSkeleton() {
328
+ return (
329
+ <div>
330
+ <Skeleton className="h-3 w-32" />
331
+ <div className="mt-3 border border-border bg-card p-6 space-y-3">
332
+ <Skeleton className="h-3.5 w-full" />
333
+ <Skeleton className="h-3.5 w-full" />
334
+ <Skeleton className="h-3.5 w-5/6" />
335
+ <Skeleton className="h-3.5 w-full" />
336
+ <Skeleton className="h-3.5 w-4/5" />
337
+ <Skeleton className="h-3.5 w-full" />
338
+ <Skeleton className="h-3.5 w-3/4" />
339
+ <Skeleton className="h-3.5 w-full" />
340
+ </div>
341
+ </div>
342
+ );
343
+ }
344
+
345
+ /** Agent vault: description + table (3 cols) */
346
+ export function AgentVaultSkeleton() {
347
+ return (
348
+ <div>
349
+ <p className="text-xs text-muted-foreground mb-4">
350
+ Service credentials this agent can access at runtime via the vault_get tool.
351
+ </p>
352
+ <TableSkeleton headers={["Service", "Type", "Label"]} rows={3} minW="min-w-[400px]" />
353
+ <div className="mt-6 flex gap-3 border border-border bg-card p-4">
354
+ <Shield className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
355
+ <div>
356
+ <p className="text-xs font-medium">Encrypted at rest</p>
357
+ <p className="mt-1 text-xs text-muted-foreground">
358
+ Values are AES-256-GCM encrypted. Never exposed in the dashboard.
359
+ </p>
360
+ </div>
361
+ </div>
362
+ </div>
363
+ );
364
+ }
365
+
366
+ /** Sessions list: heading + filter bar + rows matching the real view */
367
+ export function SessionsListSkeleton() {
368
+ return (
369
+ <div>
370
+ {/* Header */}
371
+ <div>
372
+ <h2 className="text-lg font-semibold tracking-tight">Sessions</h2>
373
+ <Skeleton className="mt-1 h-3 w-48" />
374
+ </div>
375
+ {/* Filter bar — search input, agent filter, refresh */}
376
+ <div className="mt-3 flex items-center gap-2 flex-wrap">
377
+ <Skeleton className="h-7 w-48" />
378
+ <Skeleton className="h-7 w-24" />
379
+ <Skeleton className="ml-auto h-3 w-16" />
380
+ </div>
381
+ {/* Rows */}
382
+ <div className="mt-4 border border-border overflow-hidden">
383
+ {Array.from({ length: 5 }).map((_, i) => (
384
+ <div
385
+ key={i}
386
+ className="flex items-center border-b border-border last:border-0"
387
+ >
388
+ <div className="flex-1 px-4 py-3 min-w-0 space-y-1.5">
389
+ <Skeleton className="h-3.5 w-48" />
390
+ <div className="flex items-center gap-2">
391
+ <Skeleton className="h-3 w-16 rounded" />
392
+ <Skeleton className="h-2.5 w-32" />
393
+ </div>
394
+ </div>
395
+ <div className="flex items-center gap-4 px-4 py-3 shrink-0">
396
+ <Skeleton className="h-2.5 w-14" />
397
+ <Skeleton className="h-2.5 w-14" />
398
+ </div>
399
+ <div className="px-3">
400
+ <Skeleton className="h-3 w-3 rounded-full" />
401
+ </div>
402
+ </div>
403
+ ))}
404
+ </div>
405
+ </div>
406
+ );
407
+ }
408
+
409
+ /** Session detail: back link + header + raw-view table */
410
+ export function SessionDetailSkeleton() {
411
+ return (
412
+ <div>
413
+ {/* Back link */}
414
+ <Skeleton className="h-3 w-20" />
415
+ {/* Header */}
416
+ <div className="mt-4">
417
+ <Skeleton className="h-5 w-56" />
418
+ <div className="mt-2 flex items-center gap-3">
419
+ <Skeleton className="h-3 w-40" />
420
+ <Skeleton className="h-3 w-16 rounded" />
421
+ <Skeleton className="h-3 w-32" />
422
+ <Skeleton className="h-3 w-20" />
423
+ </div>
424
+ </div>
425
+ {/* Raw-view table */}
426
+ <div className="mt-5 border border-border overflow-hidden">
427
+ {/* Header row */}
428
+ <div className="flex border-b border-border bg-muted/40 px-3 py-2 gap-3">
429
+ <Skeleton className="h-2.5 w-10" />
430
+ <Skeleton className="h-2.5 w-16" />
431
+ <Skeleton className="h-2.5 w-12" />
432
+ <Skeleton className="h-2.5 w-12" />
433
+ <Skeleton className="h-2.5 w-24 hidden md:block" />
434
+ <Skeleton className="h-2.5 flex-1" />
435
+ <Skeleton className="h-2.5 w-20" />
436
+ </div>
437
+ {/* Data rows — alternating msg + tool patterns */}
438
+ {Array.from({ length: 6 }).map((_, i) => (
439
+ <div key={i} className={`flex items-center border-b border-border last:border-0 px-3 py-2 gap-3 ${i % 3 === 2 ? "bg-muted/10" : ""}`}>
440
+ <Skeleton className="h-4 w-9 rounded" />
441
+ <Skeleton className="h-3 w-14" />
442
+ <Skeleton className="h-3 w-12" />
443
+ <Skeleton className="h-3 w-12" />
444
+ <Skeleton className="h-3 w-32 hidden md:block" />
445
+ <Skeleton className="h-3 flex-1" />
446
+ <Skeleton className="h-3 w-16" />
447
+ </div>
448
+ ))}
449
+ </div>
450
+ </div>
451
+ );
452
+ }
453
+
454
+ /** Agent memory view */
455
+ export function AgentMemorySkeleton() {
456
+ return (
457
+ <div>
458
+ <p className="text-xs text-muted-foreground mb-4">
459
+ Private memory this agent accumulates across sessions. This agent also inherits
460
+ the shared project memory.
461
+ </p>
462
+ <div className="border border-border bg-card p-6 space-y-3">
463
+ <Skeleton className="h-4 w-3/4" />
464
+ <Skeleton className="h-3.5 w-full" />
465
+ <Skeleton className="h-3.5 w-full" />
466
+ <Skeleton className="h-3.5 w-5/6" />
467
+ <Skeleton className="h-3.5 w-2/3" />
468
+ </div>
469
+ </div>
470
+ );
471
+ }
472
+
473
+ /** Project memory view — title + description live in page.tsx so the
474
+ * skeleton only renders the body placeholder (avoids double-title flash
475
+ * during streaming Suspense load). */
476
+ export function ProjectMemorySkeleton() {
477
+ return (
478
+ <div className="mt-6 border border-border bg-card p-6 space-y-3">
479
+ <Skeleton className="h-4 w-3/4" />
480
+ <Skeleton className="h-3.5 w-full" />
481
+ <Skeleton className="h-3.5 w-full" />
482
+ <Skeleton className="h-3.5 w-5/6" />
483
+ <Skeleton className="h-3.5 w-2/3" />
484
+ </div>
485
+ );
486
+ }
487
+
488
+ /** Tasks list: heading + flex list matching tasks view layout */
489
+ export function TasksListSkeleton() {
490
+ return (
491
+ <div>
492
+ <h2 className="text-lg font-semibold tracking-tight">Tasks</h2>
493
+ <Skeleton className="mt-1 h-3 w-20" />
494
+ <div className="mt-4 border border-border overflow-hidden overflow-x-auto min-w-[600px]">
495
+ {/* Header */}
496
+ <div className="flex items-center border-b border-border bg-secondary/50 text-left text-xs font-medium text-muted-foreground">
497
+ <span className="px-4 py-2.5 w-28">Status</span>
498
+ <span className="px-4 py-2.5 flex-1">Task</span>
499
+ <span className="px-4 py-2.5 w-24">Agent</span>
500
+ <span className="px-4 py-2.5 w-20">Duration</span>
501
+ <span className="px-4 py-2.5 w-16">Retries</span>
502
+ <span className="px-4 py-2.5 w-20 text-right">Updated</span>
503
+ <span className="w-8" />
504
+ </div>
505
+ {/* Rows */}
506
+ {Array.from({ length: 5 }).map((_, i) => (
507
+ <div key={i} className="flex items-center border-b border-border last:border-0">
508
+ <span className="px-4 py-3 w-28">
509
+ <span className="inline-flex items-center gap-1.5">
510
+ <Skeleton className="h-2 w-2 rounded-full" />
511
+ <Skeleton className="h-3 w-14" />
512
+ </span>
513
+ </span>
514
+ <span className="px-4 py-3 flex-1">
515
+ <Skeleton className="h-3.5 w-40" />
516
+ </span>
517
+ <span className="px-4 py-3 w-24">
518
+ <Skeleton className="h-3 w-16" />
519
+ </span>
520
+ <span className="px-4 py-3 w-20">
521
+ <Skeleton className="h-3 w-10" />
522
+ </span>
523
+ <span className="px-4 py-3 w-16">
524
+ <Skeleton className="h-3 w-8" />
525
+ </span>
526
+ <span className="px-4 py-3 w-20 text-right">
527
+ <Skeleton className="ml-auto h-3 w-12" />
528
+ </span>
529
+ <span className="px-2 py-3 w-8">
530
+ <Skeleton className="h-3.5 w-3.5 rounded" />
531
+ </span>
532
+ </div>
533
+ ))}
534
+ </div>
535
+ </div>
536
+ );
537
+ }
538
+
539
+ /** Task detail: status + title + 4 meta cards + details table */
540
+ export function TaskDetailSkeleton() {
541
+ return (
542
+ <div>
543
+ <div className="flex items-center gap-3">
544
+ <Skeleton className="h-2.5 w-2.5 rounded-full" />
545
+ <Skeleton className="h-5 w-56" />
546
+ </div>
547
+ <Skeleton className="mt-2 h-3.5 w-full max-w-md" />
548
+
549
+ <div className="mt-6 grid grid-cols-2 gap-3 sm:gap-4 sm:grid-cols-4">
550
+ <StatCardSkeleton label="Agent" />
551
+ <StatCardSkeleton label="Status" />
552
+ <StatCardSkeleton label="Phase" />
553
+ <StatCardSkeleton label="Retries" />
554
+ </div>
555
+
556
+ <div className="mt-8">
557
+ <h3 className="text-sm font-semibold">Details</h3>
558
+ <DetailTableSkeleton
559
+ rows={[
560
+ { label: "Mission" },
561
+ { label: "Dependencies" },
562
+ { label: "Side effects" },
563
+ { label: "Session" },
564
+ { label: "Created" },
565
+ { label: "Updated" },
566
+ ]}
567
+ />
568
+ </div>
569
+ </div>
570
+ );
571
+ }
572
+
573
+ /** Task output: meta bar + stdout block */
574
+ export function TaskOutputSkeleton() {
575
+ return (
576
+ <div>
577
+ <div className="flex items-center gap-6 mb-4 text-xs text-muted-foreground">
578
+ <span>Exit code: <Skeleton className="inline-block h-3 w-6 align-middle" /></span>
579
+ <span>Duration: <Skeleton className="inline-block h-3 w-10 align-middle" /></span>
580
+ </div>
581
+ <h3 className="text-sm font-semibold">stdout</h3>
582
+ <div className="mt-3 border border-border bg-card p-5 space-y-2">
583
+ <Skeleton className="h-3 w-full" />
584
+ <Skeleton className="h-3 w-full" />
585
+ <Skeleton className="h-3 w-5/6" />
586
+ <Skeleton className="h-3 w-full" />
587
+ <Skeleton className="h-3 w-3/4" />
588
+ <Skeleton className="h-3 w-full" />
589
+ </div>
590
+ </div>
591
+ );
592
+ }
593
+
594
+ /** Task assessment: summary bar + checks + score cards */
595
+ export function TaskAssessmentSkeleton() {
596
+ return (
597
+ <div>
598
+ {/* Summary */}
599
+ <div className="flex items-center justify-between">
600
+ <div className="flex items-center gap-3">
601
+ <Skeleton className="h-3 w-3 rounded-full" />
602
+ <Skeleton className="h-4 w-16" />
603
+ </div>
604
+ <Skeleton className="h-7 w-14" />
605
+ </div>
606
+
607
+ {/* Checks */}
608
+ <section className="mt-8">
609
+ <h3 className="text-sm font-semibold">Expectation checks</h3>
610
+ <div className="mt-3 border border-border overflow-hidden">
611
+ {Array.from({ length: 3 }).map((_, i) => (
612
+ <div
613
+ key={i}
614
+ className="flex items-center justify-between border-b border-border last:border-0 px-4 py-3"
615
+ >
616
+ <div className="flex items-center gap-2">
617
+ <Skeleton className="h-3 w-16" />
618
+ <Skeleton className="h-3 w-40" />
619
+ </div>
620
+ <Skeleton className="h-2 w-2 rounded-full" />
621
+ </div>
622
+ ))}
623
+ </div>
624
+ </section>
625
+
626
+ {/* Score cards */}
627
+ <section className="mt-8">
628
+ <h3 className="text-sm font-semibold">G-Eval dimensions</h3>
629
+ <div className="mt-3 space-y-3">
630
+ {Array.from({ length: 3 }).map((_, i) => (
631
+ <div
632
+ key={i}
633
+ className="border border-border bg-card p-4"
634
+ >
635
+ <div className="flex items-center justify-between">
636
+ <Skeleton className="h-4 w-24" />
637
+ <Skeleton className="h-4 w-10" />
638
+ </div>
639
+ <Skeleton className="mt-2 h-1.5 w-full rounded-full" />
640
+ <Skeleton className="mt-3 h-3 w-full" />
641
+ <Skeleton className="mt-1 h-3 w-4/5" />
642
+ </div>
643
+ ))}
644
+ </div>
645
+ </section>
646
+ </div>
647
+ );
648
+ }
649
+
650
+ /** Missions list: heading + table (6 cols) */
651
+ export function MissionsListSkeleton() {
652
+ return (
653
+ <div>
654
+ <h2 className="text-lg font-semibold tracking-tight">Missions</h2>
655
+ <Skeleton className="mt-1 h-3 w-20" />
656
+ <TableSkeleton
657
+ headers={["Status", "Mission", "Schedule", "Runs", "Quality", "Updated"]}
658
+ rows={3}
659
+ lastRight
660
+ />
661
+ </div>
662
+ );
663
+ }
664
+
665
+ /** Schedules list: heading + table matching schedules view layout */
666
+ export function SchedulesListSkeleton() {
667
+ return (
668
+ <div>
669
+ <div className="flex items-center justify-between">
670
+ <div>
671
+ <h2 className="text-lg font-semibold tracking-tight">Schedules</h2>
672
+ <Skeleton className="mt-1 h-3 w-24" />
673
+ </div>
674
+ <Skeleton className="h-3 w-16" />
675
+ </div>
676
+ <TableSkeleton
677
+ headers={["Status", "Mission", "Type", "Expression", "Next run", "Runs", "Created"]}
678
+ rows={3}
679
+ minW="min-w-[800px]"
680
+ lastRight
681
+ />
682
+ </div>
683
+ );
684
+ }
685
+
686
+ export function MissionHeaderSkeleton() {
687
+ return (
688
+ <div>
689
+ <div className="flex items-center gap-3">
690
+ <Skeleton className="h-2.5 w-2.5 rounded-full" />
691
+ <Skeleton className="h-5 w-48" />
692
+ </div>
693
+ <Skeleton className="mt-2 h-3.5 w-full max-w-lg" />
694
+
695
+ <div className="mt-4 flex flex-wrap items-center gap-4">
696
+ <Skeleton className="h-3 w-20" />
697
+ <Skeleton className="h-3 w-14" />
698
+ <Skeleton className="h-3 w-36" />
699
+ </div>
700
+
701
+ <Skeleton className="mt-6 h-8 w-28" />
702
+ </div>
703
+ );
704
+ }
705
+
706
+ export function MissionGraphSkeleton() {
707
+ return (
708
+ <section className="mt-4">
709
+ <div className="mb-4 flex flex-wrap items-center gap-4">
710
+ <Skeleton className="h-3 w-24" />
711
+ <Skeleton className="h-3 w-20" />
712
+ </div>
713
+
714
+ <div className="mb-3 flex items-center gap-1 rounded-lg bg-muted/50 p-1 w-fit">
715
+ <Skeleton className="h-7 w-20 rounded-md" />
716
+ <Skeleton className="h-7 w-20 rounded-md" />
717
+ <Skeleton className="h-7 w-20 rounded-md" />
718
+ </div>
719
+
720
+ <div className="relative h-[600px] w-full overflow-hidden rounded-xl border border-border bg-card">
721
+ <div className="absolute inset-0 opacity-40 [background-image:radial-gradient(circle_at_1px_1px,hsl(var(--muted-foreground))_1px,transparent_0)] [background-size:20px_20px]" />
722
+ <div className="relative grid h-full grid-cols-3 gap-8 p-8">
723
+ {Array.from({ length: 6 }).map((_, i) => (
724
+ <div key={i} className="flex items-center justify-center">
725
+ <div className="w-full max-w-52 rounded-lg border border-border bg-background p-4">
726
+ <div className="flex items-center gap-2">
727
+ <Skeleton className="h-2.5 w-2.5 rounded-full" />
728
+ <Skeleton className="h-3.5 w-28" />
729
+ </div>
730
+ <Skeleton className="mt-3 h-3 w-20" />
731
+ <Skeleton className="mt-2 h-2 w-full" />
732
+ </div>
733
+ </div>
734
+ ))}
735
+ </div>
736
+ </div>
737
+ </section>
738
+ );
739
+ }
740
+
741
+ /** Mission detail: header + task graph */
742
+ export function MissionDetailSkeleton() {
743
+ return (
744
+ <div>
745
+ <MissionHeaderSkeleton />
746
+ <MissionGraphSkeleton />
747
+ </div>
748
+ );
749
+ }
750
+
751
+ /** Skills list: heading + description + table (2 cols) */
752
+ export function SkillsListSkeleton() {
753
+ return (
754
+ <div>
755
+ <div className="flex items-center justify-between">
756
+ <div>
757
+ <h2 className="text-lg font-semibold tracking-tight">Skills</h2>
758
+ <Skeleton className="mt-1 h-3 w-56" />
759
+ </div>
760
+ <Skeleton className="h-4 w-16" />
761
+ </div>
762
+ <TableSkeleton headers={["Skill", "Assigned to", "Tags"]} rows={3} minW="min-w-[520px]" />
763
+ </div>
764
+ );
765
+ }
766
+
767
+ /** Webhooks: heading + description + webhook rows */
768
+ export function WebhooksListSkeleton() {
769
+ return (
770
+ <div>
771
+ <div className="flex items-center justify-between">
772
+ <div>
773
+ <h2 className="text-lg font-semibold tracking-tight">Webhooks</h2>
774
+ <p className="mt-1 text-xs text-muted-foreground">
775
+ Receive HTTP POST notifications when events occur in this project.
776
+ </p>
777
+ </div>
778
+ <Skeleton className="h-8 w-28" />
779
+ </div>
780
+ <div className="mt-4 border border-border overflow-hidden">
781
+ {Array.from({ length: 3 }).map((_, i) => (
782
+ <div
783
+ key={i}
784
+ className="flex items-center justify-between border-b border-border last:border-0 px-4 py-3"
785
+ >
786
+ <div className="min-w-0 flex-1 space-y-1.5">
787
+ <Skeleton className="h-3.5 w-64" />
788
+ <div className="flex items-center gap-2">
789
+ <Skeleton className="h-4 w-14 rounded" />
790
+ <Skeleton className="h-4 w-20 rounded" />
791
+ </div>
792
+ </div>
793
+ <Skeleton className="ml-4 h-3.5 w-3.5" />
794
+ </div>
795
+ ))}
796
+ </div>
797
+ </div>
798
+ );
799
+ }
800
+
801
+ /** Logs: heading + session sidebar + entries panel */
802
+ export function LogsViewSkeleton() {
803
+ return (
804
+ <div>
805
+ <h2 className="text-lg font-semibold tracking-tight">Logs</h2>
806
+ <p className="mt-1 text-xs text-muted-foreground">
807
+ Orchestrator session logs. Each session captures events from a single orchestrator run.
808
+ </p>
809
+ <div className="mt-6 flex gap-6">
810
+ {/* Session list sidebar */}
811
+ <div className="w-64 shrink-0">
812
+ <p className="text-xs font-medium text-muted-foreground mb-3">
813
+ <Skeleton className="inline-block h-3 w-4 align-middle" /> sessions
814
+ </p>
815
+ <div className="border border-border overflow-hidden">
816
+ {Array.from({ length: 4 }).map((_, i) => (
817
+ <div key={i} className="px-3 py-2.5 border-b border-border last:border-0 space-y-1.5">
818
+ <Skeleton className="h-3 w-24" />
819
+ <div className="flex items-center justify-between">
820
+ <Skeleton className="h-2.5 w-12" />
821
+ <Skeleton className="h-2.5 w-16" />
822
+ </div>
823
+ </div>
824
+ ))}
825
+ </div>
826
+ </div>
827
+ {/* Entries placeholder */}
828
+ <div className="flex-1 min-w-0 border border-border p-8 text-center text-sm text-muted-foreground">
829
+ Select a session to view logs.
830
+ </div>
831
+ </div>
832
+ </div>
833
+ );
834
+ }
835
+
836
+ /** Logs entries loading (sub-spinner) */
837
+ export function LogsEntriesSkeleton() {
838
+ return (
839
+ <div className="border border-border overflow-hidden">
840
+ {Array.from({ length: 6 }).map((_, i) => (
841
+ <div
842
+ key={i}
843
+ className="flex items-start gap-3 border-b border-border last:border-0 px-3 py-2"
844
+ >
845
+ <Skeleton className="h-3 w-16" />
846
+ <Skeleton className="h-3 w-40" />
847
+ <Skeleton className="h-3 w-48" />
848
+ </div>
849
+ ))}
850
+ </div>
851
+ );
852
+ }
853
+
854
+ /** Storage: file browser skeleton */
855
+ export function StorageSkeleton() {
856
+ return (
857
+ <div>
858
+ <div className="flex items-center justify-between">
859
+ <div>
860
+ <h2 className="text-lg font-semibold tracking-tight">Storage</h2>
861
+ <p className="mt-1 text-xs text-muted-foreground">Project files and directories.</p>
862
+ </div>
863
+ <Skeleton className="h-7 w-20" />
864
+ </div>
865
+ <div className="mt-4 border border-border overflow-hidden">
866
+ {Array.from({ length: 6 }).map((_, i) => (
867
+ <div key={i} className="flex items-center gap-3 border-b border-border last:border-0 px-4 py-3">
868
+ <Skeleton className="h-4 w-4" />
869
+ <Skeleton className="h-3.5 w-48" />
870
+ <Skeleton className="ml-auto h-3 w-16" />
871
+ </div>
872
+ ))}
873
+ </div>
874
+ </div>
875
+ );
876
+ }
877
+
878
+ /** Skill detail: back + heading + pills + 2-col content/files */
879
+ export function SkillDetailSkeleton() {
880
+ return (
881
+ <div>
882
+ <Skeleton className="h-3 w-12" />
883
+ <div className="mt-4">
884
+ <Skeleton className="h-5 w-48" />
885
+ <Skeleton className="mt-2 h-3.5 w-96" />
886
+ </div>
887
+ <div className="mt-4 flex flex-wrap gap-2">
888
+ <Skeleton className="h-6 w-20 rounded-md" />
889
+ <Skeleton className="h-6 w-24 rounded-md" />
890
+ <Skeleton className="h-6 w-28 rounded-md" />
891
+ </div>
892
+ <div className="mt-6 grid gap-6 lg:grid-cols-[1fr_380px]">
893
+ <div>
894
+ <Skeleton className="h-3 w-20 mb-3" />
895
+ <div className="border border-border bg-card p-6 space-y-3">
896
+ <Skeleton className="h-3.5 w-full" />
897
+ <Skeleton className="h-3.5 w-full" />
898
+ <Skeleton className="h-3.5 w-5/6" />
899
+ <Skeleton className="h-3.5 w-4/5" />
900
+ <Skeleton className="h-3.5 w-full" />
901
+ <Skeleton className="h-3.5 w-3/4" />
902
+ </div>
903
+ </div>
904
+ <div>
905
+ <Skeleton className="h-3 w-12 mb-3" />
906
+ <div className="border border-border overflow-hidden">
907
+ {Array.from({ length: 4 }).map((_, i) => (
908
+ <div key={i} className="flex items-center gap-2 border-b border-border last:border-0 px-3 py-2">
909
+ <Skeleton className="h-3.5 w-3.5" />
910
+ <Skeleton className="h-3 w-32" />
911
+ </div>
912
+ ))}
913
+ </div>
914
+ </div>
915
+ </div>
916
+ </div>
917
+ );
918
+ }
919
+
920
+ /** Projects list: toolbar (count + view toggle) + 6-card grid */
921
+ export function ProjectsListSkeleton() {
922
+ return (
923
+ <div className="mt-8">
924
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
925
+ {Array.from({ length: 6 }).map((_, i) => (
926
+ <div key={i} className="border border-border bg-card p-5">
927
+ <div className="flex items-center justify-between">
928
+ <Skeleton className="h-4 w-32" />
929
+ <Skeleton className="h-2 w-2 rounded-full" />
930
+ </div>
931
+ <Skeleton className="mt-2 h-3 w-24" />
932
+ <div className="mt-4 flex items-center justify-between">
933
+ <Skeleton className="h-2.5 w-20" />
934
+ <Skeleton className="h-2.5 w-12" />
935
+ </div>
936
+ </div>
937
+ ))}
938
+ </div>
939
+ </div>
940
+ );
941
+ }
942
+
943
+ /** API Keys table: header row + 4 rows */
944
+ export function KeysTableSkeleton() {
945
+ return (
946
+ <div className="mt-8 border border-border overflow-hidden overflow-x-auto">
947
+ <table className="w-full text-sm min-w-[600px]">
948
+ <thead>
949
+ <tr className="border-b border-border bg-secondary/50">
950
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Name</th>
951
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Scope</th>
952
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Prefix</th>
953
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Created</th>
954
+ <th className="px-4 py-2.5" />
955
+ </tr>
956
+ </thead>
957
+ <tbody>
958
+ {Array.from({ length: 4 }).map((_, i) => (
959
+ <tr key={i} className="border-b border-border last:border-0">
960
+ <td className="px-4 py-3"><Skeleton className="h-3.5 w-32" /></td>
961
+ <td className="px-4 py-3"><Skeleton className="h-4 w-16 rounded" /></td>
962
+ <td className="px-4 py-3"><Skeleton className="h-3 w-24" /></td>
963
+ <td className="px-4 py-3"><Skeleton className="h-3 w-20" /></td>
964
+ <td className="px-4 py-3"><Skeleton className="h-3.5 w-3.5 rounded" /></td>
965
+ </tr>
966
+ ))}
967
+ </tbody>
968
+ </table>
969
+ </div>
970
+ );
971
+ }
972
+
973
+ /** LLM Gateway: gateway-per-project table + add button */
974
+ export function LlmGatewaySkeleton() {
975
+ return (
976
+ <div className="mt-6">
977
+ <div className="border border-border overflow-hidden overflow-x-auto">
978
+ <table className="w-full text-sm min-w-[500px]">
979
+ <thead>
980
+ <tr className="border-b border-border bg-secondary/50">
981
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Project</th>
982
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Gateway</th>
983
+ <th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">URL</th>
984
+ <th className="px-4 py-2.5 text-right text-xs font-medium text-muted-foreground"></th>
985
+ </tr>
986
+ </thead>
987
+ <tbody>
988
+ {Array.from({ length: 2 }).map((_, i) => (
989
+ <tr key={i} className="border-b border-border last:border-0">
990
+ <td className="px-4 py-3"><Skeleton className="h-3 w-24" /></td>
991
+ <td className="px-4 py-3"><Skeleton className="h-4 w-14 rounded" /></td>
992
+ <td className="px-4 py-3"><Skeleton className="h-3 w-40" /></td>
993
+ <td className="px-4 py-3" />
994
+ </tr>
995
+ ))}
996
+ </tbody>
997
+ </table>
998
+ </div>
999
+ <Skeleton className="mt-4 h-9 w-44" />
1000
+ </div>
1001
+ );
1002
+ }
1003
+
1004
+ /** Model catalog: title + filters bar + table rows */
1005
+ export function ModelCatalogSkeleton() {
1006
+ return (
1007
+ <div className="mt-10">
1008
+ <Skeleton className="h-5 w-20" />
1009
+ <Skeleton className="mt-2 h-3 w-80" />
1010
+ <div className="mt-4 flex flex-wrap items-center gap-3">
1011
+ <Skeleton className="h-9 w-64" />
1012
+ <Skeleton className="h-9 w-72" />
1013
+ <Skeleton className="h-9 w-32" />
1014
+ </div>
1015
+ <div className="mt-4 border border-border overflow-hidden">
1016
+ {Array.from({ length: 6 }).map((_, i) => (
1017
+ <div key={i} className="flex items-center gap-4 border-b border-border last:border-0 px-4 py-3">
1018
+ <Skeleton className="h-3.5 w-48" />
1019
+ <Skeleton className="h-3 w-12" />
1020
+ <Skeleton className="ml-auto h-3 w-16" />
1021
+ <Skeleton className="h-3 w-16" />
1022
+ <Skeleton className="h-3 w-20" />
1023
+ </div>
1024
+ ))}
1025
+ </div>
1026
+ </div>
1027
+ );
1028
+ }
1029
+
1030
+ /** BYOK: header + table (3 cols) + encryption card */
1031
+ export function ByokSkeleton() {
1032
+ return (
1033
+ <div>
1034
+ <div className="flex items-center justify-between">
1035
+ <div>
1036
+ <h2 className="text-lg font-semibold tracking-tight">LLM Keys</h2>
1037
+ <p className="mt-1 text-xs text-muted-foreground">
1038
+ Your LLM API keys, AES-256 encrypted per project.
1039
+ </p>
1040
+ </div>
1041
+ </div>
1042
+ <TableSkeleton headers={["Provider", "Label", "Key"]} rows={3} minW="min-w-[400px]" />
1043
+ <div className="mt-6 flex gap-3 border border-border bg-card p-4">
1044
+ <Shield className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
1045
+ <div>
1046
+ <p className="text-xs font-medium">End-to-end encrypted</p>
1047
+ <p className="mt-1 text-xs text-muted-foreground">
1048
+ Keys are encrypted with AES-256-GCM before storage. We never see your keys in plaintext.
1049
+ </p>
1050
+ </div>
1051
+ </div>
1052
+ </div>
1053
+ );
1054
+ }