@ash-ai/dashboard 0.0.1
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/LICENSE +21 -0
- package/app/agents/page.tsx +408 -0
- package/app/analytics/page.tsx +226 -0
- package/app/globals.css +33 -0
- package/app/layout.tsx +38 -0
- package/app/logs/page.tsx +233 -0
- package/app/page.tsx +140 -0
- package/app/playground/page.tsx +44 -0
- package/app/queue/page.tsx +295 -0
- package/app/sessions/page.tsx +529 -0
- package/app/settings/api-keys/page.tsx +222 -0
- package/app/settings/credentials/page.tsx +250 -0
- package/components/nav.tsx +151 -0
- package/components/providers.tsx +18 -0
- package/components/ui/badge.tsx +43 -0
- package/components/ui/button.tsx +44 -0
- package/components/ui/card.tsx +43 -0
- package/components/ui/empty-state.tsx +20 -0
- package/components/ui/input.tsx +36 -0
- package/components/ui/select.tsx +50 -0
- package/components/ui/shimmer.tsx +53 -0
- package/lib/client.ts +41 -0
- package/lib/exports.ts +55 -0
- package/lib/hooks.ts +169 -0
- package/lib/utils.ts +44 -0
- package/next.config.ts +28 -0
- package/out/404/index.html +1 -0
- package/out/404.html +1 -0
- package/out/_next/static/J9asKIV7Gq221ygeAP958/_buildManifest.js +1 -0
- package/out/_next/static/J9asKIV7Gq221ygeAP958/_ssgManifest.js +1 -0
- package/out/_next/static/chunks/322-bab4df5c5188e993.js +1 -0
- package/out/_next/static/chunks/432-11ec8af7ccfbd019.js +1 -0
- package/out/_next/static/chunks/447.6d3368efa2d996b0.js +1 -0
- package/out/_next/static/chunks/513-c4683887323154aa.js +1 -0
- package/out/_next/static/chunks/522-cf174cf1bbbe9557.js +1 -0
- package/out/_next/static/chunks/53-b012ce05184a4754.js +1 -0
- package/out/_next/static/chunks/929-6faf1adeb65ee383.js +1 -0
- package/out/_next/static/chunks/app/_not-found/page-04f9d3958a76bc38.js +1 -0
- package/out/_next/static/chunks/app/agents/page-8d68e3019b4d7077.js +1 -0
- package/out/_next/static/chunks/app/analytics/page-6b725a46e9c48019.js +1 -0
- package/out/_next/static/chunks/app/layout-9cae773a790a15b2.js +1 -0
- package/out/_next/static/chunks/app/logs/page-2efd945345a44a0e.js +1 -0
- package/out/_next/static/chunks/app/page-06f62e11f9cd82d5.js +1 -0
- package/out/_next/static/chunks/app/playground/page-10d3461f118bfb21.js +1 -0
- package/out/_next/static/chunks/app/queue/page-38e79b84cbd59335.js +1 -0
- package/out/_next/static/chunks/app/sessions/page-2a67c9eddfac029e.js +1 -0
- package/out/_next/static/chunks/app/settings/api-keys/page-619682cf8a1c26eb.js +1 -0
- package/out/_next/static/chunks/app/settings/credentials/page-106d0ba4f12afe81.js +1 -0
- package/out/_next/static/chunks/b59f762a-cea625f74e98e0aa.js +1 -0
- package/out/_next/static/chunks/framework-5a02266cf144994c.js +1 -0
- package/out/_next/static/chunks/main-994a6af9cdcd7fb9.js +1 -0
- package/out/_next/static/chunks/main-app-5bcd4dcc44c4ae09.js +1 -0
- package/out/_next/static/chunks/pages/_app-9fd734050704698a.js +1 -0
- package/out/_next/static/chunks/pages/_error-310ed5880fd5d5e6.js +1 -0
- package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/out/_next/static/chunks/webpack-a50a78a04aed446d.js +1 -0
- package/out/_next/static/css/4b2beada31dbc623.css +1 -0
- package/out/agents/index.html +1 -0
- package/out/agents/index.txt +22 -0
- package/out/analytics/index.html +1 -0
- package/out/analytics/index.txt +22 -0
- package/out/index.html +1 -0
- package/out/index.txt +22 -0
- package/out/logs/index.html +1 -0
- package/out/logs/index.txt +22 -0
- package/out/playground/index.html +1 -0
- package/out/playground/index.txt +22 -0
- package/out/queue/index.html +1 -0
- package/out/queue/index.txt +22 -0
- package/out/sessions/index.html +1 -0
- package/out/sessions/index.txt +22 -0
- package/out/settings/api-keys/index.html +1 -0
- package/out/settings/api-keys/index.txt +22 -0
- package/out/settings/credentials/index.html +1 -0
- package/out/settings/credentials/index.txt +22 -0
- package/package.json +40 -0
- package/postcss.config.mjs +7 -0
- package/tsconfig.json +27 -0
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import Script from 'next/script'
|
|
3
|
+
import './globals.css'
|
|
4
|
+
import { DashboardNav } from '@/components/nav'
|
|
5
|
+
import { AshProvider } from '@/components/providers'
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: 'Ash Dashboard',
|
|
9
|
+
description: 'Manage agents, sessions, and monitor your Ash server',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" className="dark">
|
|
19
|
+
<head>
|
|
20
|
+
{/* Injects window.__ASH_CONFIG__ with API key + server version.
|
|
21
|
+
beforeInteractive ensures it loads before React hydration. */}
|
|
22
|
+
<Script src="/dashboard/config.js" strategy="beforeInteractive" />
|
|
23
|
+
</head>
|
|
24
|
+
<body className="bg-[#0d1117] text-zinc-100 antialiased">
|
|
25
|
+
<AshProvider>
|
|
26
|
+
<div className="flex min-h-screen">
|
|
27
|
+
<DashboardNav />
|
|
28
|
+
<main className="min-w-0 flex-1 pl-64 overflow-y-auto overflow-x-hidden">
|
|
29
|
+
<div className="mx-auto max-w-[1600px] px-6 pb-6 pt-8 sm:px-8 sm:pb-8 sm:pt-10">
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
</main>
|
|
33
|
+
</div>
|
|
34
|
+
</AshProvider>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react'
|
|
4
|
+
import { useAgents, useSessions } from '@/lib/hooks'
|
|
5
|
+
import { getClient } from '@/lib/client'
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import { Badge, StatusBadge } from '@/components/ui/badge'
|
|
9
|
+
import { Select } from '@/components/ui/select'
|
|
10
|
+
import { Input } from '@/components/ui/input'
|
|
11
|
+
import { ShimmerBlock } from '@/components/ui/shimmer'
|
|
12
|
+
import { cn, formatRelativeTime, truncateId } from '@/lib/utils'
|
|
13
|
+
import { ChevronDown, ChevronRight, Download, RefreshCw, Search } from 'lucide-react'
|
|
14
|
+
import type { Session, SessionEvent } from '@ash-ai/shared'
|
|
15
|
+
|
|
16
|
+
export default function LogsPage() {
|
|
17
|
+
const { agents } = useAgents()
|
|
18
|
+
const [agentFilter, setAgentFilter] = useState('')
|
|
19
|
+
const [statusFilter, setStatusFilter] = useState('')
|
|
20
|
+
const [search, setSearch] = useState('')
|
|
21
|
+
const [autoRefresh, setAutoRefresh] = useState(true)
|
|
22
|
+
|
|
23
|
+
const { sessions, loading, refetch } = useSessions({
|
|
24
|
+
agent: agentFilter || undefined,
|
|
25
|
+
autoRefresh,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const filtered = sessions.filter((s) => {
|
|
29
|
+
if (statusFilter && s.status !== statusFilter) return false
|
|
30
|
+
if (search) {
|
|
31
|
+
const q = search.toLowerCase()
|
|
32
|
+
return (
|
|
33
|
+
s.id.toLowerCase().includes(q) ||
|
|
34
|
+
s.agentName.toLowerCase().includes(q) ||
|
|
35
|
+
s.status.toLowerCase().includes(q)
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
return true
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
function exportCSV() {
|
|
42
|
+
const rows = [
|
|
43
|
+
['Session ID', 'Agent', 'Status', 'Created'].join(','),
|
|
44
|
+
...filtered.map((s) =>
|
|
45
|
+
[s.id, s.agentName, s.status, s.createdAt].join(',')
|
|
46
|
+
),
|
|
47
|
+
]
|
|
48
|
+
const blob = new Blob([rows.join('\n')], { type: 'text/csv' })
|
|
49
|
+
const url = URL.createObjectURL(blob)
|
|
50
|
+
const a = document.createElement('a')
|
|
51
|
+
a.href = url
|
|
52
|
+
a.download = 'sessions.csv'
|
|
53
|
+
a.click()
|
|
54
|
+
URL.revokeObjectURL(url)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function exportJSON() {
|
|
58
|
+
const blob = new Blob([JSON.stringify(filtered, null, 2)], {
|
|
59
|
+
type: 'application/json',
|
|
60
|
+
})
|
|
61
|
+
const url = URL.createObjectURL(blob)
|
|
62
|
+
const a = document.createElement('a')
|
|
63
|
+
a.href = url
|
|
64
|
+
a.download = 'sessions.json'
|
|
65
|
+
a.click()
|
|
66
|
+
URL.revokeObjectURL(url)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="space-y-6">
|
|
71
|
+
<div className="flex items-center justify-between">
|
|
72
|
+
<div>
|
|
73
|
+
<h1 className="text-2xl font-bold text-white">Logs</h1>
|
|
74
|
+
<p className="mt-1 text-sm text-white/50">
|
|
75
|
+
Session activity and event explorer
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex items-center gap-2">
|
|
79
|
+
<Button variant="ghost" size="sm" onClick={exportCSV}>
|
|
80
|
+
<Download className="h-4 w-4 mr-1" /> CSV
|
|
81
|
+
</Button>
|
|
82
|
+
<Button variant="ghost" size="sm" onClick={exportJSON}>
|
|
83
|
+
<Download className="h-4 w-4 mr-1" /> JSON
|
|
84
|
+
</Button>
|
|
85
|
+
<Button
|
|
86
|
+
variant={autoRefresh ? 'secondary' : 'ghost'}
|
|
87
|
+
size="sm"
|
|
88
|
+
onClick={() => setAutoRefresh(!autoRefresh)}
|
|
89
|
+
>
|
|
90
|
+
<RefreshCw className={cn('h-4 w-4', autoRefresh && 'animate-spin')} />
|
|
91
|
+
</Button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Filters */}
|
|
96
|
+
<div className="flex gap-3">
|
|
97
|
+
<div className="flex-1 relative">
|
|
98
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-white/30" />
|
|
99
|
+
<input
|
|
100
|
+
type="text"
|
|
101
|
+
placeholder="Search sessions..."
|
|
102
|
+
value={search}
|
|
103
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
104
|
+
className="w-full h-9 pl-9 pr-3 rounded-xl border bg-white/5 border-white/10 text-sm text-white placeholder:text-white/40 focus-visible:outline-none focus-visible:border-indigo-500/50"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
<Select
|
|
108
|
+
options={[
|
|
109
|
+
{ value: '', label: 'All Agents' },
|
|
110
|
+
...agents.map((a) => ({ value: a.name, label: a.name })),
|
|
111
|
+
]}
|
|
112
|
+
value={agentFilter}
|
|
113
|
+
onChange={(e) => setAgentFilter(e.target.value)}
|
|
114
|
+
className="w-40 h-9 text-xs"
|
|
115
|
+
/>
|
|
116
|
+
<Select
|
|
117
|
+
options={[
|
|
118
|
+
{ value: '', label: 'All Statuses' },
|
|
119
|
+
{ value: 'active', label: 'Active' },
|
|
120
|
+
{ value: 'paused', label: 'Paused' },
|
|
121
|
+
{ value: 'ended', label: 'Ended' },
|
|
122
|
+
{ value: 'error', label: 'Error' },
|
|
123
|
+
]}
|
|
124
|
+
value={statusFilter}
|
|
125
|
+
onChange={(e) => setStatusFilter(e.target.value)}
|
|
126
|
+
className="w-36 h-9 text-xs"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Table */}
|
|
131
|
+
<Card>
|
|
132
|
+
<CardContent className="p-0">
|
|
133
|
+
{loading ? (
|
|
134
|
+
<div className="p-4 space-y-2">
|
|
135
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
136
|
+
<ShimmerBlock key={i} height={40} />
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
) : filtered.length === 0 ? (
|
|
140
|
+
<p className="text-sm text-white/40 text-center py-12">
|
|
141
|
+
No sessions found
|
|
142
|
+
</p>
|
|
143
|
+
) : (
|
|
144
|
+
<div className="divide-y divide-white/5">
|
|
145
|
+
{filtered.map((session) => (
|
|
146
|
+
<SessionRow key={session.id} session={session} />
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</CardContent>
|
|
151
|
+
</Card>
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function SessionRow({ session }: { session: Session }) {
|
|
157
|
+
const [expanded, setExpanded] = useState(false)
|
|
158
|
+
const [events, setEvents] = useState<SessionEvent[]>([])
|
|
159
|
+
const [loadingEvents, setLoadingEvents] = useState(false)
|
|
160
|
+
|
|
161
|
+
async function toggleExpand() {
|
|
162
|
+
if (!expanded && events.length === 0) {
|
|
163
|
+
setLoadingEvents(true)
|
|
164
|
+
try {
|
|
165
|
+
const result = await getClient().listSessionEvents(session.id, { limit: 50 })
|
|
166
|
+
setEvents(result)
|
|
167
|
+
} catch {
|
|
168
|
+
// ignore
|
|
169
|
+
} finally {
|
|
170
|
+
setLoadingEvents(false)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
setExpanded(!expanded)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div>
|
|
178
|
+
<button
|
|
179
|
+
onClick={toggleExpand}
|
|
180
|
+
aria-expanded={expanded}
|
|
181
|
+
className="flex items-center gap-4 w-full px-4 py-3 text-sm hover:bg-white/[0.02] transition-colors"
|
|
182
|
+
>
|
|
183
|
+
{expanded ? (
|
|
184
|
+
<ChevronDown className="h-4 w-4 text-white/30 shrink-0" />
|
|
185
|
+
) : (
|
|
186
|
+
<ChevronRight className="h-4 w-4 text-white/30 shrink-0" />
|
|
187
|
+
)}
|
|
188
|
+
<span className="text-xs text-white/30 w-24">
|
|
189
|
+
{formatRelativeTime(session.createdAt)}
|
|
190
|
+
</span>
|
|
191
|
+
<span className="text-xs font-mono text-white/50 w-20">
|
|
192
|
+
{truncateId(session.id)}
|
|
193
|
+
</span>
|
|
194
|
+
<span className="text-sm text-white/70 flex-1 text-left truncate">
|
|
195
|
+
{session.agentName}
|
|
196
|
+
</span>
|
|
197
|
+
<StatusBadge status={session.status} />
|
|
198
|
+
</button>
|
|
199
|
+
{expanded && (
|
|
200
|
+
<div className="pl-12 pr-4 pb-4">
|
|
201
|
+
{loadingEvents ? (
|
|
202
|
+
<div className="space-y-1">
|
|
203
|
+
{[1, 2, 3].map((i) => (
|
|
204
|
+
<ShimmerBlock key={i} height={24} />
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
) : events.length === 0 ? (
|
|
208
|
+
<p className="text-xs text-white/30 py-2">No events</p>
|
|
209
|
+
) : (
|
|
210
|
+
<div className="space-y-0.5 max-h-64 overflow-auto scrollbar-thin">
|
|
211
|
+
{events.map((e, i) => {
|
|
212
|
+
let summary = ''
|
|
213
|
+
try {
|
|
214
|
+
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
|
|
215
|
+
summary = data?.text?.slice(0, 80) || data?.name || data?.error?.slice(0, 80) || ''
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
return (
|
|
220
|
+
<div key={e.id || i} className="flex items-center gap-3 text-xs py-1">
|
|
221
|
+
<span className="text-white/20 font-mono w-6">#{e.sequence}</span>
|
|
222
|
+
<Badge>{e.type}</Badge>
|
|
223
|
+
<span className="text-white/40 truncate">{summary}</span>
|
|
224
|
+
</div>
|
|
225
|
+
)
|
|
226
|
+
})}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useAgents } from '@/lib/hooks'
|
|
4
|
+
import { useSessions } from '@/lib/hooks'
|
|
5
|
+
import { useHealth } from '@/lib/hooks'
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
7
|
+
import { StatusBadge } from '@/components/ui/badge'
|
|
8
|
+
import { ShimmerBlock } from '@/components/ui/shimmer'
|
|
9
|
+
import { formatRelativeTime } from '@/lib/utils'
|
|
10
|
+
import { Activity, Bot, Cpu, Zap } from 'lucide-react'
|
|
11
|
+
import Link from 'next/link'
|
|
12
|
+
|
|
13
|
+
export default function DashboardHome() {
|
|
14
|
+
const { agents, loading: agentsLoading } = useAgents()
|
|
15
|
+
const { sessions, loading: sessionsLoading } = useSessions({ limit: 5 })
|
|
16
|
+
const { health } = useHealth()
|
|
17
|
+
|
|
18
|
+
const activeSessions = sessions.filter((s) => s.status === 'active')
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-8">
|
|
22
|
+
<div>
|
|
23
|
+
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
|
|
24
|
+
<p className="mt-1 text-sm text-white/50">
|
|
25
|
+
Overview of your Ash server
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
{/* Stat cards */}
|
|
30
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
31
|
+
<StatCard
|
|
32
|
+
icon={<Bot className="h-5 w-5" />}
|
|
33
|
+
label="Agents"
|
|
34
|
+
value={agentsLoading ? '-' : agents.length.toString()}
|
|
35
|
+
href="/agents"
|
|
36
|
+
/>
|
|
37
|
+
<StatCard
|
|
38
|
+
icon={<Activity className="h-5 w-5" />}
|
|
39
|
+
label="Active Sessions"
|
|
40
|
+
value={sessionsLoading ? '-' : activeSessions.length.toString()}
|
|
41
|
+
href="/sessions"
|
|
42
|
+
/>
|
|
43
|
+
<StatCard
|
|
44
|
+
icon={<Zap className="h-5 w-5" />}
|
|
45
|
+
label="Total Sessions"
|
|
46
|
+
value={sessionsLoading ? '-' : sessions.length.toString()}
|
|
47
|
+
href="/sessions"
|
|
48
|
+
/>
|
|
49
|
+
<StatCard
|
|
50
|
+
icon={<Cpu className="h-5 w-5" />}
|
|
51
|
+
label="Sandboxes"
|
|
52
|
+
value={health?.activeSandboxes?.toString() ?? '-'}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Recent Sessions */}
|
|
57
|
+
<Card>
|
|
58
|
+
<CardHeader>
|
|
59
|
+
<div className="flex items-center justify-between">
|
|
60
|
+
<CardTitle>Recent Sessions</CardTitle>
|
|
61
|
+
<Link
|
|
62
|
+
href="/sessions"
|
|
63
|
+
className="text-sm text-indigo-400 hover:text-indigo-300 transition-colors"
|
|
64
|
+
>
|
|
65
|
+
View all
|
|
66
|
+
</Link>
|
|
67
|
+
</div>
|
|
68
|
+
</CardHeader>
|
|
69
|
+
<CardContent>
|
|
70
|
+
{sessionsLoading ? (
|
|
71
|
+
<div className="space-y-3">
|
|
72
|
+
{[1, 2, 3].map((i) => (
|
|
73
|
+
<ShimmerBlock key={i} height={48} />
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
) : sessions.length === 0 ? (
|
|
77
|
+
<p className="text-sm text-white/40 py-8 text-center">
|
|
78
|
+
No sessions yet. Create an agent and start a session in the Playground.
|
|
79
|
+
</p>
|
|
80
|
+
) : (
|
|
81
|
+
<div className="space-y-2">
|
|
82
|
+
{sessions.slice(0, 5).map((session) => (
|
|
83
|
+
<Link
|
|
84
|
+
key={session.id}
|
|
85
|
+
href={`/sessions?id=${session.id}`}
|
|
86
|
+
className="flex items-center justify-between rounded-lg border border-white/5 bg-white/[0.02] px-4 py-3 hover:bg-white/5 transition-colors"
|
|
87
|
+
>
|
|
88
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
89
|
+
<div className="min-w-0">
|
|
90
|
+
<p className="text-sm font-medium text-white truncate">
|
|
91
|
+
{session.agentName}
|
|
92
|
+
</p>
|
|
93
|
+
<p className="text-xs text-white/40 font-mono">
|
|
94
|
+
{session.id.slice(0, 8)}
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="flex items-center gap-3">
|
|
99
|
+
<StatusBadge status={session.status} />
|
|
100
|
+
<span className="text-xs text-white/30">
|
|
101
|
+
{formatRelativeTime(session.createdAt)}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
</Link>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function StatCard({
|
|
115
|
+
icon,
|
|
116
|
+
label,
|
|
117
|
+
value,
|
|
118
|
+
href,
|
|
119
|
+
}: {
|
|
120
|
+
icon: React.ReactNode
|
|
121
|
+
label: string
|
|
122
|
+
value: string
|
|
123
|
+
href?: string
|
|
124
|
+
}) {
|
|
125
|
+
const content = (
|
|
126
|
+
<Card className={href ? 'hover:border-white/20 cursor-pointer' : ''}>
|
|
127
|
+
<CardContent>
|
|
128
|
+
<div className="flex items-center justify-between">
|
|
129
|
+
<div>
|
|
130
|
+
<p className="text-sm text-white/50">{label}</p>
|
|
131
|
+
<p className="text-2xl font-bold text-white mt-1">{value}</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="text-white/20">{icon}</div>
|
|
134
|
+
</div>
|
|
135
|
+
</CardContent>
|
|
136
|
+
</Card>
|
|
137
|
+
)
|
|
138
|
+
if (href) return <Link href={href}>{content}</Link>
|
|
139
|
+
return content
|
|
140
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Suspense } from 'react'
|
|
4
|
+
import { useSearchParams } from 'next/navigation'
|
|
5
|
+
import { getClient } from '@/lib/client'
|
|
6
|
+
|
|
7
|
+
// Dynamic import to avoid SSR issues with @ash-ai/ui
|
|
8
|
+
import dynamic from 'next/dynamic'
|
|
9
|
+
|
|
10
|
+
const Playground = dynamic(
|
|
11
|
+
() => import('@ash-ai/ui').then((mod) => ({ default: mod.Playground })),
|
|
12
|
+
{ ssr: false, loading: () => <PlaygroundSkeleton /> }
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
function PlaygroundSkeleton() {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex h-full items-center justify-center">
|
|
18
|
+
<div className="text-sm text-white/40">Loading playground...</div>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function PlaygroundInner() {
|
|
24
|
+
const searchParams = useSearchParams()
|
|
25
|
+
const initialAgent = searchParams.get('agent') || undefined
|
|
26
|
+
const client = getClient()
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="h-[calc(100vh-5rem)]">
|
|
30
|
+
<Playground
|
|
31
|
+
client={client}
|
|
32
|
+
{...(initialAgent ? { initialAgent } : {})}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function PlaygroundPage() {
|
|
39
|
+
return (
|
|
40
|
+
<Suspense fallback={<PlaygroundSkeleton />}>
|
|
41
|
+
<PlaygroundInner />
|
|
42
|
+
</Suspense>
|
|
43
|
+
)
|
|
44
|
+
}
|