@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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/app/agents/page.tsx +408 -0
  3. package/app/analytics/page.tsx +226 -0
  4. package/app/globals.css +33 -0
  5. package/app/layout.tsx +38 -0
  6. package/app/logs/page.tsx +233 -0
  7. package/app/page.tsx +140 -0
  8. package/app/playground/page.tsx +44 -0
  9. package/app/queue/page.tsx +295 -0
  10. package/app/sessions/page.tsx +529 -0
  11. package/app/settings/api-keys/page.tsx +222 -0
  12. package/app/settings/credentials/page.tsx +250 -0
  13. package/components/nav.tsx +151 -0
  14. package/components/providers.tsx +18 -0
  15. package/components/ui/badge.tsx +43 -0
  16. package/components/ui/button.tsx +44 -0
  17. package/components/ui/card.tsx +43 -0
  18. package/components/ui/empty-state.tsx +20 -0
  19. package/components/ui/input.tsx +36 -0
  20. package/components/ui/select.tsx +50 -0
  21. package/components/ui/shimmer.tsx +53 -0
  22. package/lib/client.ts +41 -0
  23. package/lib/exports.ts +55 -0
  24. package/lib/hooks.ts +169 -0
  25. package/lib/utils.ts +44 -0
  26. package/next.config.ts +28 -0
  27. package/out/404/index.html +1 -0
  28. package/out/404.html +1 -0
  29. package/out/_next/static/J9asKIV7Gq221ygeAP958/_buildManifest.js +1 -0
  30. package/out/_next/static/J9asKIV7Gq221ygeAP958/_ssgManifest.js +1 -0
  31. package/out/_next/static/chunks/322-bab4df5c5188e993.js +1 -0
  32. package/out/_next/static/chunks/432-11ec8af7ccfbd019.js +1 -0
  33. package/out/_next/static/chunks/447.6d3368efa2d996b0.js +1 -0
  34. package/out/_next/static/chunks/513-c4683887323154aa.js +1 -0
  35. package/out/_next/static/chunks/522-cf174cf1bbbe9557.js +1 -0
  36. package/out/_next/static/chunks/53-b012ce05184a4754.js +1 -0
  37. package/out/_next/static/chunks/929-6faf1adeb65ee383.js +1 -0
  38. package/out/_next/static/chunks/app/_not-found/page-04f9d3958a76bc38.js +1 -0
  39. package/out/_next/static/chunks/app/agents/page-8d68e3019b4d7077.js +1 -0
  40. package/out/_next/static/chunks/app/analytics/page-6b725a46e9c48019.js +1 -0
  41. package/out/_next/static/chunks/app/layout-9cae773a790a15b2.js +1 -0
  42. package/out/_next/static/chunks/app/logs/page-2efd945345a44a0e.js +1 -0
  43. package/out/_next/static/chunks/app/page-06f62e11f9cd82d5.js +1 -0
  44. package/out/_next/static/chunks/app/playground/page-10d3461f118bfb21.js +1 -0
  45. package/out/_next/static/chunks/app/queue/page-38e79b84cbd59335.js +1 -0
  46. package/out/_next/static/chunks/app/sessions/page-2a67c9eddfac029e.js +1 -0
  47. package/out/_next/static/chunks/app/settings/api-keys/page-619682cf8a1c26eb.js +1 -0
  48. package/out/_next/static/chunks/app/settings/credentials/page-106d0ba4f12afe81.js +1 -0
  49. package/out/_next/static/chunks/b59f762a-cea625f74e98e0aa.js +1 -0
  50. package/out/_next/static/chunks/framework-5a02266cf144994c.js +1 -0
  51. package/out/_next/static/chunks/main-994a6af9cdcd7fb9.js +1 -0
  52. package/out/_next/static/chunks/main-app-5bcd4dcc44c4ae09.js +1 -0
  53. package/out/_next/static/chunks/pages/_app-9fd734050704698a.js +1 -0
  54. package/out/_next/static/chunks/pages/_error-310ed5880fd5d5e6.js +1 -0
  55. package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  56. package/out/_next/static/chunks/webpack-a50a78a04aed446d.js +1 -0
  57. package/out/_next/static/css/4b2beada31dbc623.css +1 -0
  58. package/out/agents/index.html +1 -0
  59. package/out/agents/index.txt +22 -0
  60. package/out/analytics/index.html +1 -0
  61. package/out/analytics/index.txt +22 -0
  62. package/out/index.html +1 -0
  63. package/out/index.txt +22 -0
  64. package/out/logs/index.html +1 -0
  65. package/out/logs/index.txt +22 -0
  66. package/out/playground/index.html +1 -0
  67. package/out/playground/index.txt +22 -0
  68. package/out/queue/index.html +1 -0
  69. package/out/queue/index.txt +22 -0
  70. package/out/sessions/index.html +1 -0
  71. package/out/sessions/index.txt +22 -0
  72. package/out/settings/api-keys/index.html +1 -0
  73. package/out/settings/api-keys/index.txt +22 -0
  74. package/out/settings/credentials/index.html +1 -0
  75. package/out/settings/credentials/index.txt +22 -0
  76. package/package.json +40 -0
  77. package/postcss.config.mjs +7 -0
  78. 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
+ }