@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
@@ -0,0 +1,222 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { getAuthHeaders } from '@/lib/client'
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Input } from '@/components/ui/input'
8
+ import { Badge } from '@/components/ui/badge'
9
+ import { ShimmerBlock } from '@/components/ui/shimmer'
10
+ import { formatRelativeTime } from '@/lib/utils'
11
+ import { Copy, Key, Plus, Trash2 } from 'lucide-react'
12
+
13
+ interface ApiKey {
14
+ id: string
15
+ label: string
16
+ keyPrefix?: string
17
+ createdAt: string
18
+ }
19
+
20
+ export default function ApiKeysPage() {
21
+ const [keys, setKeys] = useState<ApiKey[]>([])
22
+ const [loading, setLoading] = useState(true)
23
+ const [newKeyName, setNewKeyName] = useState('')
24
+ const [createdKey, setCreatedKey] = useState<string | null>(null)
25
+ const [creating, setCreating] = useState(false)
26
+ const [error, setError] = useState<string | null>(null)
27
+
28
+ async function fetchKeys() {
29
+ try {
30
+ const res = await fetch('/api/api-keys', {
31
+ headers: getAuthHeaders(),
32
+ })
33
+ if (res.ok) {
34
+ const data = await res.json()
35
+ setKeys(data.keys || data || [])
36
+ }
37
+ } catch {
38
+ // Endpoint may not exist yet
39
+ } finally {
40
+ setLoading(false)
41
+ }
42
+ }
43
+
44
+ useEffect(() => {
45
+ fetchKeys()
46
+ }, [])
47
+
48
+ async function handleCreate() {
49
+ if (!newKeyName.trim()) {
50
+ setError('Key name is required')
51
+ return
52
+ }
53
+
54
+ setCreating(true)
55
+ setError(null)
56
+
57
+ try {
58
+ const res = await fetch('/api/api-keys', {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ ...getAuthHeaders(),
63
+ },
64
+ body: JSON.stringify({ label: newKeyName.trim() }),
65
+ })
66
+
67
+ if (!res.ok) throw new Error('Failed to create key')
68
+
69
+ const data = await res.json()
70
+ setCreatedKey(data.key)
71
+ setNewKeyName('')
72
+ fetchKeys()
73
+ } catch (e) {
74
+ const message = e instanceof Error ? e.message : 'Failed to create key'
75
+ setError(message)
76
+ } finally {
77
+ setCreating(false)
78
+ }
79
+ }
80
+
81
+ async function handleRevoke(id: string) {
82
+ try {
83
+ const res = await fetch(`/api/api-keys/${id}`, {
84
+ method: 'DELETE',
85
+ headers: getAuthHeaders(),
86
+ })
87
+ if (!res.ok) throw new Error('Failed to revoke key')
88
+ fetchKeys()
89
+ } catch (e) {
90
+ const message = e instanceof Error ? e.message : 'Failed to revoke key'
91
+ setError(message)
92
+ }
93
+ }
94
+
95
+ return (
96
+ <div className="space-y-6">
97
+ <div>
98
+ <h1 className="text-2xl font-bold text-white">API Keys</h1>
99
+ <p className="mt-1 text-sm text-white/50">
100
+ Create API keys to authenticate with the Ash server
101
+ </p>
102
+ </div>
103
+
104
+ {/* Create key */}
105
+ <Card>
106
+ <CardContent>
107
+ <div className="flex gap-3">
108
+ <Input
109
+ placeholder="Key name (e.g. production)"
110
+ value={newKeyName}
111
+ onChange={(e) => setNewKeyName(e.target.value)}
112
+ className="flex-1"
113
+ onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
114
+ />
115
+ <Button onClick={handleCreate} disabled={creating}>
116
+ <Plus className="h-4 w-4 mr-2" />
117
+ {creating ? 'Creating...' : 'Create Key'}
118
+ </Button>
119
+ </div>
120
+ {error && <p className="text-sm text-red-400 mt-2">{error}</p>}
121
+ </CardContent>
122
+ </Card>
123
+
124
+ {/* Newly created key */}
125
+ {createdKey && (
126
+ <Card className="border-green-500/30">
127
+ <CardContent>
128
+ <div className="flex items-center gap-2 mb-2">
129
+ <Key className="h-4 w-4 text-green-400" />
130
+ <p className="text-sm font-medium text-green-400">
131
+ Key created! Copy it now — it won&apos;t be shown again.
132
+ </p>
133
+ </div>
134
+ <div className="flex items-center gap-2 bg-black/30 rounded-lg px-4 py-3">
135
+ <code className="flex-1 text-sm text-white font-mono break-all">
136
+ {createdKey}
137
+ </code>
138
+ <Button
139
+ variant="ghost"
140
+ size="sm"
141
+ onClick={() => {
142
+ navigator.clipboard.writeText(createdKey)
143
+ }}
144
+ >
145
+ <Copy className="h-4 w-4" />
146
+ </Button>
147
+ </div>
148
+ </CardContent>
149
+ </Card>
150
+ )}
151
+
152
+ {/* Getting started */}
153
+ <Card>
154
+ <CardHeader>
155
+ <CardTitle>Getting Started</CardTitle>
156
+ </CardHeader>
157
+ <CardContent>
158
+ <div className="rounded-lg bg-black/30 p-4 font-mono text-sm text-white/80 space-y-1">
159
+ <div className="text-white/40"># Set your API key</div>
160
+ <div>export ASH_API_KEY=ash_sk_...</div>
161
+ <div className="text-white/40 mt-3"># Deploy an agent</div>
162
+ <div>ash deploy ./my-agent --name my-agent</div>
163
+ </div>
164
+ </CardContent>
165
+ </Card>
166
+
167
+ {/* Key list */}
168
+ <Card>
169
+ <CardHeader>
170
+ <CardTitle>Active Keys</CardTitle>
171
+ </CardHeader>
172
+ <CardContent className="p-0">
173
+ {loading ? (
174
+ <div className="p-4 space-y-2">
175
+ {[1, 2, 3].map((i) => (
176
+ <ShimmerBlock key={i} height={40} />
177
+ ))}
178
+ </div>
179
+ ) : keys.length === 0 ? (
180
+ <p className="text-sm text-white/40 text-center py-8">
181
+ No API keys yet
182
+ </p>
183
+ ) : (
184
+ <table className="w-full text-sm">
185
+ <thead>
186
+ <tr className="border-b border-white/10">
187
+ <th className="text-left px-6 py-3 text-xs font-medium text-white/40 uppercase">Name</th>
188
+ <th className="text-left px-6 py-3 text-xs font-medium text-white/40 uppercase">Key</th>
189
+ <th className="text-left px-6 py-3 text-xs font-medium text-white/40 uppercase">Created</th>
190
+ <th className="text-right px-6 py-3 text-xs font-medium text-white/40 uppercase">Actions</th>
191
+ </tr>
192
+ </thead>
193
+ <tbody className="divide-y divide-white/5">
194
+ {keys.map((key) => (
195
+ <tr key={key.id} className="hover:bg-white/[0.02]">
196
+ <td className="px-6 py-3 text-white/80 font-medium">{key.label}</td>
197
+ <td className="px-6 py-3 text-white/40 font-mono text-xs">
198
+ {key.keyPrefix || '••••••••'}
199
+ </td>
200
+ <td className="px-6 py-3 text-white/40">
201
+ {formatRelativeTime(key.createdAt)}
202
+ </td>
203
+ <td className="px-6 py-3 text-right">
204
+ <Button
205
+ variant="ghost"
206
+ size="sm"
207
+ onClick={() => handleRevoke(key.id)}
208
+ className="text-red-400 hover:text-red-300"
209
+ >
210
+ <Trash2 className="h-3.5 w-3.5" />
211
+ </Button>
212
+ </td>
213
+ </tr>
214
+ ))}
215
+ </tbody>
216
+ </table>
217
+ )}
218
+ </CardContent>
219
+ </Card>
220
+ </div>
221
+ )
222
+ }
@@ -0,0 +1,250 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useCredentials } 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 { Input } from '@/components/ui/input'
9
+ import { Select } from '@/components/ui/select'
10
+ import { Badge } from '@/components/ui/badge'
11
+ import { EmptyState } from '@/components/ui/empty-state'
12
+ import { ShimmerBlock } from '@/components/ui/shimmer'
13
+ import { formatRelativeTime } from '@/lib/utils'
14
+ import { Lock, Plus, Trash2, X } from 'lucide-react'
15
+
16
+ const CREDENTIAL_TYPES = [
17
+ { value: 'anthropic', label: 'Anthropic' },
18
+ { value: 'openai', label: 'OpenAI' },
19
+ { value: 'bedrock', label: 'AWS Bedrock' },
20
+ { value: 'custom', label: 'Custom' },
21
+ ]
22
+
23
+ export default function CredentialsPage() {
24
+ const { credentials, loading, refetch } = useCredentials()
25
+ const [showCreate, setShowCreate] = useState(false)
26
+ const [error, setError] = useState<string | null>(null)
27
+
28
+ async function handleDelete(id: string) {
29
+ setError(null)
30
+ try {
31
+ await getClient().deleteCredential(id)
32
+ refetch()
33
+ } catch (e) {
34
+ const message = e instanceof Error ? e.message : 'Failed to delete credential'
35
+ setError(message)
36
+ }
37
+ }
38
+
39
+ return (
40
+ <div className="space-y-6">
41
+ <div className="flex items-center justify-between">
42
+ <div>
43
+ <h1 className="text-2xl font-bold text-white">Credentials</h1>
44
+ <p className="mt-1 text-sm text-white/50">
45
+ Store API keys for LLM providers. Encrypted at rest.
46
+ </p>
47
+ </div>
48
+ <Button onClick={() => setShowCreate(true)}>
49
+ <Plus className="h-4 w-4 mr-2" />
50
+ Add Credential
51
+ </Button>
52
+ </div>
53
+
54
+ {error && (
55
+ <div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-4 py-2">
56
+ {error}
57
+ </div>
58
+ )}
59
+
60
+ <Card>
61
+ <CardContent className="p-0">
62
+ {loading ? (
63
+ <div className="p-4 space-y-2">
64
+ {[1, 2, 3].map((i) => (
65
+ <ShimmerBlock key={i} height={48} />
66
+ ))}
67
+ </div>
68
+ ) : credentials.length === 0 ? (
69
+ <EmptyState
70
+ icon={<Lock className="h-12 w-12" />}
71
+ title="No credentials stored"
72
+ description="Add API keys for LLM providers to use with your agents."
73
+ action={
74
+ <Button onClick={() => setShowCreate(true)}>
75
+ <Plus className="h-4 w-4 mr-2" />
76
+ Add Credential
77
+ </Button>
78
+ }
79
+ />
80
+ ) : (
81
+ <table className="w-full text-sm">
82
+ <thead>
83
+ <tr className="border-b border-white/10">
84
+ <th className="text-left px-6 py-3 text-xs font-medium text-white/40 uppercase">Provider</th>
85
+ <th className="text-left px-6 py-3 text-xs font-medium text-white/40 uppercase">Label</th>
86
+ <th className="text-left px-6 py-3 text-xs font-medium text-white/40 uppercase">Status</th>
87
+ <th className="text-left px-6 py-3 text-xs font-medium text-white/40 uppercase">Last Used</th>
88
+ <th className="text-right px-6 py-3 text-xs font-medium text-white/40 uppercase">Actions</th>
89
+ </tr>
90
+ </thead>
91
+ <tbody className="divide-y divide-white/5">
92
+ {credentials.map((cred) => (
93
+ <tr key={cred.id} className="hover:bg-white/[0.02]">
94
+ <td className="px-6 py-3">
95
+ <Badge variant="info">{cred.type}</Badge>
96
+ </td>
97
+ <td className="px-6 py-3 text-white/80">{cred.label || '-'}</td>
98
+ <td className="px-6 py-3">
99
+ <Badge variant={cred.active ? 'success' : 'default'}>
100
+ {cred.active ? 'Active' : 'Inactive'}
101
+ </Badge>
102
+ </td>
103
+ <td className="px-6 py-3 text-white/40">
104
+ {cred.lastUsedAt ? formatRelativeTime(cred.lastUsedAt) : 'Never'}
105
+ </td>
106
+ <td className="px-6 py-3 text-right">
107
+ <Button
108
+ variant="ghost"
109
+ size="sm"
110
+ onClick={() => handleDelete(cred.id)}
111
+ className="text-red-400 hover:text-red-300"
112
+ >
113
+ <Trash2 className="h-3.5 w-3.5" />
114
+ </Button>
115
+ </td>
116
+ </tr>
117
+ ))}
118
+ </tbody>
119
+ </table>
120
+ )}
121
+ </CardContent>
122
+ </Card>
123
+
124
+ {showCreate && (
125
+ <CreateCredentialModal
126
+ onClose={() => setShowCreate(false)}
127
+ onCreated={() => {
128
+ setShowCreate(false)
129
+ refetch()
130
+ }}
131
+ />
132
+ )}
133
+ </div>
134
+ )
135
+ }
136
+
137
+ function CreateCredentialModal({
138
+ onClose,
139
+ onCreated,
140
+ }: {
141
+ onClose: () => void
142
+ onCreated: () => void
143
+ }) {
144
+ const [type, setType] = useState('anthropic')
145
+ const [label, setLabel] = useState('')
146
+ const [apiKey, setApiKey] = useState('')
147
+ const [accessKeyId, setAccessKeyId] = useState('')
148
+ const [secretKey, setSecretKey] = useState('')
149
+ const [region, setRegion] = useState('us-east-1')
150
+ const [creating, setCreating] = useState(false)
151
+ const [error, setError] = useState<string | null>(null)
152
+
153
+ async function handleCreate() {
154
+ setCreating(true)
155
+ setError(null)
156
+
157
+ try {
158
+ const client = getClient()
159
+ if (type === 'bedrock') {
160
+ await client.storeBedrockCredential({
161
+ accessKeyId,
162
+ secretAccessKey: secretKey,
163
+ region,
164
+ label: label || undefined,
165
+ })
166
+ } else {
167
+ await client.storeCredential(type, apiKey, label || undefined)
168
+ }
169
+ onCreated()
170
+ } catch (e) {
171
+ const message = e instanceof Error ? e.message : 'Failed to store credential'
172
+ setError(message)
173
+ } finally {
174
+ setCreating(false)
175
+ }
176
+ }
177
+
178
+ return (
179
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
180
+ <Card className="w-full max-w-md">
181
+ <CardContent>
182
+ <div className="flex items-center justify-between mb-6">
183
+ <h2 className="text-lg font-semibold text-white">Add Credential</h2>
184
+ <button onClick={onClose} className="text-white/40 hover:text-white">
185
+ <X className="h-5 w-5" />
186
+ </button>
187
+ </div>
188
+
189
+ <div className="space-y-4">
190
+ <Select
191
+ label="Provider"
192
+ options={CREDENTIAL_TYPES}
193
+ value={type}
194
+ onChange={(e) => setType(e.target.value)}
195
+ />
196
+
197
+ <Input
198
+ label="Label (optional)"
199
+ placeholder="e.g. production"
200
+ value={label}
201
+ onChange={(e) => setLabel(e.target.value)}
202
+ />
203
+
204
+ {type === 'bedrock' ? (
205
+ <>
206
+ <Input
207
+ label="Access Key ID"
208
+ placeholder="AKIA..."
209
+ value={accessKeyId}
210
+ onChange={(e) => setAccessKeyId(e.target.value)}
211
+ />
212
+ <Input
213
+ label="Secret Access Key"
214
+ type="password"
215
+ value={secretKey}
216
+ onChange={(e) => setSecretKey(e.target.value)}
217
+ />
218
+ <Input
219
+ label="Region"
220
+ placeholder="us-east-1"
221
+ value={region}
222
+ onChange={(e) => setRegion(e.target.value)}
223
+ />
224
+ </>
225
+ ) : (
226
+ <Input
227
+ label="API Key"
228
+ type="password"
229
+ placeholder={type === 'anthropic' ? 'sk-ant-...' : 'sk-...'}
230
+ value={apiKey}
231
+ onChange={(e) => setApiKey(e.target.value)}
232
+ />
233
+ )}
234
+
235
+ {error && <p className="text-sm text-red-400">{error}</p>}
236
+
237
+ <div className="flex justify-end gap-3 pt-2">
238
+ <Button variant="ghost" onClick={onClose}>
239
+ Cancel
240
+ </Button>
241
+ <Button onClick={handleCreate} disabled={creating}>
242
+ {creating ? 'Saving...' : 'Save'}
243
+ </Button>
244
+ </div>
245
+ </div>
246
+ </CardContent>
247
+ </Card>
248
+ </div>
249
+ )
250
+ }
@@ -0,0 +1,151 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import { cn } from '@/lib/utils'
6
+ import { useHealth } from '@/lib/hooks'
7
+ import {
8
+ Activity,
9
+ Bot,
10
+ Code2,
11
+ Key,
12
+ LayoutDashboard,
13
+ ListOrdered,
14
+ Lock,
15
+ ScrollText,
16
+ Settings,
17
+ TrendingUp,
18
+ } from 'lucide-react'
19
+
20
+ export interface NavItem {
21
+ href: string
22
+ label: string
23
+ icon: React.ComponentType<{ className?: string }>
24
+ external?: boolean
25
+ }
26
+
27
+ interface DashboardNavProps {
28
+ extraItems?: NavItem[]
29
+ extraBottomLinks?: NavItem[]
30
+ branding?: string
31
+ }
32
+
33
+ const navItems: NavItem[] = [
34
+ { href: '/', label: 'Dashboard', icon: LayoutDashboard },
35
+ { href: '/playground', label: 'Playground', icon: Code2 },
36
+ { href: '/agents', label: 'Agents', icon: Bot },
37
+ { href: '/sessions', label: 'Sessions', icon: Activity },
38
+ { href: '/logs', label: 'Logs', icon: ScrollText },
39
+ { href: '/analytics', label: 'Analytics', icon: TrendingUp },
40
+ ]
41
+
42
+ const bottomLinks: NavItem[] = [
43
+ { href: '/settings/api-keys', label: 'API Keys', icon: Key },
44
+ { href: '/settings/credentials', label: 'Credentials', icon: Lock },
45
+ { href: '/queue', label: 'Queue', icon: ListOrdered },
46
+ ]
47
+
48
+ export function DashboardNav({ extraItems, extraBottomLinks, branding }: DashboardNavProps) {
49
+ const pathname = usePathname()
50
+ const { health } = useHealth()
51
+
52
+ const allItems = extraItems ? [...navItems, ...extraItems] : navItems
53
+ const allBottomLinks = extraBottomLinks ? [...bottomLinks, ...extraBottomLinks] : bottomLinks
54
+
55
+ const isHealthy = health?.status === 'ok'
56
+
57
+ return (
58
+ <nav className="fixed inset-y-0 left-0 z-50 w-64 flex flex-col border-r border-white/10 bg-[#0d1117]">
59
+ {/* Logo */}
60
+ <div className="flex h-16 items-center gap-3 px-5 border-b border-white/10">
61
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-indigo-500">
62
+ <span className="text-sm font-bold text-white">A</span>
63
+ </div>
64
+ <div className="min-w-0">
65
+ <span className="text-white font-bold tracking-tight block">
66
+ {branding || 'Ash'}
67
+ </span>
68
+ <div className="flex items-center gap-1.5 text-white/40 text-xs font-mono">
69
+ <span
70
+ className={cn(
71
+ 'w-1.5 h-1.5 rounded-full',
72
+ isHealthy ? 'bg-green-400 animate-pulse' : 'bg-red-400'
73
+ )}
74
+ />
75
+ {isHealthy ? 'ONLINE' : 'OFFLINE'}
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ {/* Main nav */}
81
+ <div className="flex-1 overflow-auto px-3 py-4 scrollbar-thin">
82
+ <div className="space-y-0.5">
83
+ {allItems.map((item) => {
84
+ const isActive =
85
+ pathname === item.href ||
86
+ (item.href !== '/' && pathname.startsWith(item.href))
87
+ return (
88
+ <Link
89
+ key={item.href}
90
+ href={item.href}
91
+ className={cn(
92
+ 'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200',
93
+ isActive
94
+ ? 'bg-indigo-500/10 text-indigo-400 border border-indigo-500/30'
95
+ : 'text-white/60 hover:text-white hover:bg-white/5 border border-transparent'
96
+ )}
97
+ >
98
+ <item.icon className="h-4 w-4" />
99
+ {item.label}
100
+ {isActive && (
101
+ <div className="ml-auto w-1.5 h-1.5 rounded-full bg-indigo-400 animate-pulse" />
102
+ )}
103
+ </Link>
104
+ )
105
+ })}
106
+ </div>
107
+ </div>
108
+
109
+ {/* Bottom links */}
110
+ <div className="border-t border-white/10 px-3 py-3 space-y-0.5">
111
+ {allBottomLinks.map((item) => {
112
+ const isActive = !item.external && pathname.startsWith(item.href)
113
+ if (item.external) {
114
+ return (
115
+ <a
116
+ key={item.label}
117
+ href={item.href}
118
+ target="_blank"
119
+ rel="noopener noreferrer"
120
+ className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-white/40 hover:text-white hover:bg-white/5 transition-all duration-200"
121
+ >
122
+ <item.icon className="h-4 w-4" />
123
+ {item.label}
124
+ </a>
125
+ )
126
+ }
127
+ return (
128
+ <Link
129
+ key={item.label}
130
+ href={item.href}
131
+ className={cn(
132
+ 'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200',
133
+ isActive
134
+ ? 'text-indigo-400'
135
+ : 'text-white/40 hover:text-white hover:bg-white/5'
136
+ )}
137
+ >
138
+ <item.icon className="h-4 w-4" />
139
+ {item.label}
140
+ </Link>
141
+ )
142
+ })}
143
+ {health?.version && (
144
+ <div className="px-3 py-2 text-xs text-white/20 font-mono">
145
+ v{health.version}
146
+ </div>
147
+ )}
148
+ </div>
149
+ </nav>
150
+ )
151
+ }
@@ -0,0 +1,18 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext } from 'react'
4
+ import type { AshClient } from '@ash-ai/sdk'
5
+ import { getClient } from '@/lib/client'
6
+
7
+ const AshContext = createContext<AshClient | null>(null)
8
+
9
+ export function AshProvider({ children }: { children: React.ReactNode }) {
10
+ const client = getClient()
11
+ return <AshContext.Provider value={client}>{children}</AshContext.Provider>
12
+ }
13
+
14
+ export function useAshClient(): AshClient {
15
+ const client = useContext(AshContext)
16
+ if (!client) throw new Error('useAshClient must be used within AshProvider')
17
+ return client
18
+ }
@@ -0,0 +1,43 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ export interface BadgeProps {
4
+ children: React.ReactNode
5
+ variant?: 'default' | 'success' | 'warning' | 'error' | 'info'
6
+ className?: string
7
+ }
8
+
9
+ const variants = {
10
+ default: 'bg-white/10 text-white/70 border border-white/10',
11
+ success: 'bg-green-500/20 text-green-400 border border-green-500/30',
12
+ warning: 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30',
13
+ error: 'bg-red-500/20 text-red-400 border border-red-500/30',
14
+ info: 'bg-blue-500/20 text-blue-400 border border-blue-500/30',
15
+ }
16
+
17
+ export function Badge({ children, variant = 'default', className }: BadgeProps) {
18
+ return (
19
+ <span
20
+ className={cn(
21
+ 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
22
+ variants[variant],
23
+ className
24
+ )}
25
+ >
26
+ {children}
27
+ </span>
28
+ )
29
+ }
30
+
31
+ export function StatusBadge({ status }: { status: string }) {
32
+ const variant =
33
+ ({
34
+ active: 'success',
35
+ starting: 'info',
36
+ paused: 'warning',
37
+ stopped: 'default',
38
+ ended: 'default',
39
+ error: 'error',
40
+ } as Record<string, BadgeProps['variant']>)[status] || 'default'
41
+
42
+ return <Badge variant={variant}>{status}</Badge>
43
+ }