@ash-ai/dashboard 0.0.6 → 0.0.7

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/app/agents/detail/page.tsx +182 -0
  2. package/app/agents/eval-compare/page.tsx +445 -0
  3. package/app/agents/eval-run/page.tsx +331 -0
  4. package/app/agents/eval-runs/page.tsx +376 -0
  5. package/app/agents/evals/page.tsx +448 -0
  6. package/app/agents/knowledge/page.tsx +202 -0
  7. package/app/agents/page.tsx +5 -3
  8. package/app/agents/versions/page.tsx +280 -0
  9. package/lib/hooks.ts +120 -0
  10. package/out/404/index.html +1 -1
  11. package/out/404.html +1 -1
  12. package/out/_next/static/chunks/839-186b95b4d095e127.js +1 -0
  13. package/out/_next/static/chunks/90-8e47c31ab931867f.js +1 -0
  14. package/out/_next/static/chunks/{929-6faf1adeb65ee383.js → 929-b631fe082fe4e852.js} +1 -1
  15. package/out/_next/static/chunks/app/agents/detail/page-7427483b9c74ad63.js +1 -0
  16. package/out/_next/static/chunks/app/agents/eval-compare/page-bc4e8051ba07e56f.js +1 -0
  17. package/out/_next/static/chunks/app/agents/eval-run/page-a38dac74d1b3787d.js +1 -0
  18. package/out/_next/static/chunks/app/agents/eval-runs/page-af2fb33c0fce7934.js +1 -0
  19. package/out/_next/static/chunks/app/agents/evals/page-6bdc4b839a7a8eda.js +1 -0
  20. package/out/_next/static/chunks/app/agents/knowledge/page-0e02b14bfa2a6d04.js +1 -0
  21. package/out/_next/static/chunks/app/agents/page-99f179eb7c41ebd4.js +1 -0
  22. package/out/_next/static/chunks/app/agents/versions/page-c482d9bad8f35df6.js +1 -0
  23. package/out/_next/static/chunks/app/analytics/page-ca5d8c60e62118ed.js +1 -0
  24. package/out/_next/static/chunks/app/layout-b06d1caafc026d0c.js +1 -0
  25. package/out/_next/static/chunks/app/logs/page-1a7df17a605f36d3.js +1 -0
  26. package/out/_next/static/chunks/app/page-9e02cb0e8897ab5d.js +1 -0
  27. package/out/_next/static/chunks/app/playground/{page-10d3461f118bfb21.js → page-cb17c2ffaeb31b4e.js} +1 -1
  28. package/out/_next/static/chunks/app/queue/page-6013b93817822c75.js +1 -0
  29. package/out/_next/static/chunks/app/sessions/page-add67d96ab66b690.js +1 -0
  30. package/out/_next/static/chunks/app/settings/credentials/page-ffe97ffb2f60229d.js +1 -0
  31. package/out/_next/static/css/ab505eeeff3f7df5.css +1 -0
  32. package/out/_next/static/sXYgh3eUKXRKt1T_1T3tk/_buildManifest.js +1 -0
  33. package/out/agents/detail/index.html +1 -0
  34. package/out/agents/detail/index.txt +22 -0
  35. package/out/agents/eval-compare/index.html +1 -0
  36. package/out/agents/eval-compare/index.txt +22 -0
  37. package/out/agents/eval-run/index.html +1 -0
  38. package/out/agents/eval-run/index.txt +22 -0
  39. package/out/agents/eval-runs/index.html +1 -0
  40. package/out/agents/eval-runs/index.txt +22 -0
  41. package/out/agents/evals/index.html +1 -0
  42. package/out/agents/evals/index.txt +22 -0
  43. package/out/agents/index.html +1 -1
  44. package/out/agents/index.txt +6 -6
  45. package/out/agents/knowledge/index.html +1 -0
  46. package/out/agents/knowledge/index.txt +22 -0
  47. package/out/agents/versions/index.html +1 -0
  48. package/out/agents/versions/index.txt +22 -0
  49. package/out/analytics/index.html +1 -1
  50. package/out/analytics/index.txt +6 -6
  51. package/out/index.html +1 -1
  52. package/out/index.txt +6 -6
  53. package/out/logs/index.html +1 -1
  54. package/out/logs/index.txt +6 -6
  55. package/out/playground/index.html +1 -1
  56. package/out/playground/index.txt +6 -6
  57. package/out/queue/index.html +1 -1
  58. package/out/queue/index.txt +6 -6
  59. package/out/sessions/index.html +1 -1
  60. package/out/sessions/index.txt +6 -6
  61. package/out/settings/api-keys/index.html +1 -1
  62. package/out/settings/api-keys/index.txt +6 -6
  63. package/out/settings/credentials/index.html +1 -1
  64. package/out/settings/credentials/index.txt +6 -6
  65. package/package.json +4 -4
  66. package/out/_next/static/_fEfzU87y-4u457akpPDC/_buildManifest.js +0 -1
  67. package/out/_next/static/chunks/322-bab4df5c5188e993.js +0 -1
  68. package/out/_next/static/chunks/432-11ec8af7ccfbd019.js +0 -1
  69. package/out/_next/static/chunks/app/agents/page-5f872b5fa12d7854.js +0 -1
  70. package/out/_next/static/chunks/app/analytics/page-bb296f848e25a94f.js +0 -1
  71. package/out/_next/static/chunks/app/layout-f5d1d76b525135c7.js +0 -1
  72. package/out/_next/static/chunks/app/logs/page-5165b556d13654ae.js +0 -1
  73. package/out/_next/static/chunks/app/page-d1e6d7bff1216f08.js +0 -1
  74. package/out/_next/static/chunks/app/queue/page-50142f2cfb3664e7.js +0 -1
  75. package/out/_next/static/chunks/app/sessions/page-7f55a0a4ba0be458.js +0 -1
  76. package/out/_next/static/chunks/app/settings/credentials/page-deb5556bfe57b8b9.js +0 -1
  77. package/out/_next/static/css/15bfa5d891bcf58c.css +0 -1
  78. /package/out/_next/static/{_fEfzU87y-4u457akpPDC → sXYgh3eUKXRKt1T_1T3tk}/_ssgManifest.js +0 -0
@@ -0,0 +1,448 @@
1
+ 'use client'
2
+
3
+ import { Suspense, useState } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { useEvalCases } from '@/lib/hooks'
7
+ import { getClient } from '@/lib/client'
8
+ import { Card, CardContent } from '@/components/ui/card'
9
+ import { Button } from '@/components/ui/button'
10
+ import { Input } from '@/components/ui/input'
11
+ import { Badge } from '@/components/ui/badge'
12
+ import { EmptyState } from '@/components/ui/empty-state'
13
+ import { ShimmerBlock } from '@/components/ui/shimmer'
14
+ import {
15
+ ArrowLeft,
16
+ FlaskConical,
17
+ Plus,
18
+ Trash2,
19
+ Pencil,
20
+ Download,
21
+ Upload,
22
+ Play,
23
+ X,
24
+ Loader2,
25
+ ChevronDown,
26
+ ChevronRight,
27
+ } from 'lucide-react'
28
+
29
+ function EvalsContent() {
30
+ const searchParams = useSearchParams()
31
+ const name = searchParams.get('name')
32
+ const { cases, loading, refresh } = useEvalCases(name)
33
+ const [showCreate, setShowCreate] = useState(false)
34
+ const [editCase, setEditCase] = useState<any | null>(null)
35
+ const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null)
36
+ const [expandedCase, setExpandedCase] = useState<string | null>(null)
37
+ const [importing, setImporting] = useState(false)
38
+ const [error, setError] = useState<string | null>(null)
39
+
40
+ async function handleDelete(caseId: string) {
41
+ if (!name) return
42
+ setError(null)
43
+ try {
44
+ await getClient().deleteEvalCase(name, caseId)
45
+ setDeleteConfirm(null)
46
+ refresh()
47
+ } catch (e) {
48
+ setError(e instanceof Error ? e.message : 'Failed to delete eval case')
49
+ setDeleteConfirm(null)
50
+ }
51
+ }
52
+
53
+ async function handleExport() {
54
+ if (!name) return
55
+ setError(null)
56
+ try {
57
+ const data = await getClient().exportEvalCases(name)
58
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
59
+ const url = URL.createObjectURL(blob)
60
+ const a = document.createElement('a')
61
+ a.href = url
62
+ a.download = `${name}-eval-cases.json`
63
+ a.click()
64
+ URL.revokeObjectURL(url)
65
+ } catch (e) {
66
+ setError(e instanceof Error ? e.message : 'Failed to export eval cases')
67
+ }
68
+ }
69
+
70
+ async function handleImport(fileList: FileList) {
71
+ if (!name || fileList.length === 0) return
72
+ setImporting(true)
73
+ setError(null)
74
+ try {
75
+ const text = await fileList[0].text()
76
+ const parsed = JSON.parse(text)
77
+ const casesToImport = Array.isArray(parsed) ? parsed : parsed.cases || []
78
+ await getClient().importEvalCases(name, casesToImport)
79
+ refresh()
80
+ } catch (e) {
81
+ setError(e instanceof Error ? e.message : 'Failed to import eval cases')
82
+ } finally {
83
+ setImporting(false)
84
+ }
85
+ }
86
+
87
+ if (!name) {
88
+ return (
89
+ <div className="text-center py-16">
90
+ <p className="text-white/50">No agent name specified.</p>
91
+ <Link href="/agents" className="text-indigo-400 hover:text-indigo-300 text-sm mt-2 inline-block">
92
+ Back to agents
93
+ </Link>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ return (
99
+ <div className="space-y-6">
100
+ {/* Back link */}
101
+ <Link
102
+ href={`/agents/detail?name=${encodeURIComponent(name)}`}
103
+ className="inline-flex items-center gap-1.5 text-sm text-white/50 hover:text-white transition-colors"
104
+ >
105
+ <ArrowLeft className="h-4 w-4" />
106
+ Back to {name}
107
+ </Link>
108
+
109
+ <div className="flex items-center justify-between">
110
+ <div>
111
+ <h1 className="text-2xl font-bold text-white">Eval Cases</h1>
112
+ <p className="mt-1 text-sm text-white/50">
113
+ Test cases for <span className="text-white/70">{name}</span>
114
+ </p>
115
+ </div>
116
+ <div className="flex items-center gap-2">
117
+ <Link href={`/agents/eval-runs?name=${encodeURIComponent(name)}`}>
118
+ <Button variant="secondary">
119
+ <Play className="h-4 w-4 mr-2" />
120
+ Eval Runs
121
+ </Button>
122
+ </Link>
123
+ <Button variant="secondary" onClick={handleExport}>
124
+ <Download className="h-4 w-4 mr-2" />
125
+ Export
126
+ </Button>
127
+ <label className="cursor-pointer">
128
+ <span className="inline-flex items-center justify-center font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/50 disabled:pointer-events-none disabled:opacity-50 border border-white/20 bg-white/5 text-white hover:bg-white/10 hover:border-white/30 h-9 px-4 text-sm rounded-xl">
129
+ <Upload className="h-4 w-4 mr-2" />
130
+ {importing ? 'Importing...' : 'Import'}
131
+ </span>
132
+ <input
133
+ type="file"
134
+ accept=".json"
135
+ className="hidden"
136
+ onChange={(e) => {
137
+ if (e.target.files) {
138
+ handleImport(e.target.files)
139
+ e.target.value = ''
140
+ }
141
+ }}
142
+ />
143
+ </label>
144
+ <Button onClick={() => setShowCreate(true)}>
145
+ <Plus className="h-4 w-4 mr-2" />
146
+ Add Case
147
+ </Button>
148
+ </div>
149
+ </div>
150
+
151
+ {error && (
152
+ <div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-4 py-2">
153
+ {error}
154
+ </div>
155
+ )}
156
+
157
+ {loading ? (
158
+ <div className="space-y-3">
159
+ {[1, 2, 3].map((i) => (
160
+ <ShimmerBlock key={i} height={70} />
161
+ ))}
162
+ </div>
163
+ ) : cases.length === 0 ? (
164
+ <EmptyState
165
+ icon={<FlaskConical className="h-12 w-12" />}
166
+ title="No eval cases yet"
167
+ description="Create test cases to evaluate your agent's responses. Define questions, expected topics, and reference answers."
168
+ action={
169
+ <Button onClick={() => setShowCreate(true)}>
170
+ <Plus className="h-4 w-4 mr-2" />
171
+ Add Eval Case
172
+ </Button>
173
+ }
174
+ />
175
+ ) : (
176
+ <div className="space-y-2">
177
+ {cases.map((evalCase: any) => {
178
+ const isExpanded = expandedCase === evalCase.id
179
+ return (
180
+ <Card key={evalCase.id}>
181
+ <CardContent className="!py-3">
182
+ <div className="flex items-center justify-between">
183
+ <button
184
+ onClick={() => setExpandedCase(isExpanded ? null : evalCase.id)}
185
+ className="flex items-center gap-2 min-w-0 flex-1 text-left"
186
+ >
187
+ {isExpanded ? (
188
+ <ChevronDown className="h-4 w-4 text-white/40 flex-shrink-0" />
189
+ ) : (
190
+ <ChevronRight className="h-4 w-4 text-white/40 flex-shrink-0" />
191
+ )}
192
+ <span className="text-sm text-white truncate">{evalCase.question}</span>
193
+ </button>
194
+ <div className="flex items-center gap-2 flex-shrink-0 ml-2">
195
+ {evalCase.category && (
196
+ <Badge variant="info">{evalCase.category}</Badge>
197
+ )}
198
+ {!evalCase.isActive && (
199
+ <Badge variant="warning">Inactive</Badge>
200
+ )}
201
+ <Button
202
+ variant="ghost"
203
+ size="sm"
204
+ onClick={() => setEditCase(evalCase)}
205
+ className="text-white/30 hover:text-white"
206
+ >
207
+ <Pencil className="h-3.5 w-3.5" />
208
+ </Button>
209
+ <Button
210
+ variant="ghost"
211
+ size="sm"
212
+ onClick={() => setDeleteConfirm(evalCase.id)}
213
+ className="text-white/30 hover:text-red-400"
214
+ >
215
+ <Trash2 className="h-3.5 w-3.5" />
216
+ </Button>
217
+ </div>
218
+ </div>
219
+ {isExpanded && (
220
+ <div className="mt-3 pt-3 border-t border-white/5 space-y-2">
221
+ {evalCase.expectedTopics && evalCase.expectedTopics.length > 0 && (
222
+ <div>
223
+ <span className="text-xs font-medium text-white/40">Expected topics: </span>
224
+ <span className="text-xs text-white/60">
225
+ {evalCase.expectedTopics.join(', ')}
226
+ </span>
227
+ </div>
228
+ )}
229
+ {evalCase.expectedNotTopics && evalCase.expectedNotTopics.length > 0 && (
230
+ <div>
231
+ <span className="text-xs font-medium text-white/40">Should NOT mention: </span>
232
+ <span className="text-xs text-white/60">
233
+ {evalCase.expectedNotTopics.join(', ')}
234
+ </span>
235
+ </div>
236
+ )}
237
+ {evalCase.referenceAnswer && (
238
+ <div>
239
+ <span className="text-xs font-medium text-white/40">Reference answer: </span>
240
+ <p className="text-xs text-white/60 mt-0.5">{evalCase.referenceAnswer}</p>
241
+ </div>
242
+ )}
243
+ {evalCase.tags && evalCase.tags.length > 0 && (
244
+ <div className="flex items-center gap-1 mt-1">
245
+ {evalCase.tags.map((tag: string) => (
246
+ <Badge key={tag} variant="default">{tag}</Badge>
247
+ ))}
248
+ </div>
249
+ )}
250
+ </div>
251
+ )}
252
+ </CardContent>
253
+ </Card>
254
+ )
255
+ })}
256
+ </div>
257
+ )}
258
+
259
+ {/* Create / Edit Modal */}
260
+ {(showCreate || editCase) && (
261
+ <EvalCaseModal
262
+ agentName={name}
263
+ evalCase={editCase}
264
+ onClose={() => {
265
+ setShowCreate(false)
266
+ setEditCase(null)
267
+ }}
268
+ onSaved={() => {
269
+ setShowCreate(false)
270
+ setEditCase(null)
271
+ refresh()
272
+ }}
273
+ />
274
+ )}
275
+
276
+ {/* Delete Confirmation */}
277
+ {deleteConfirm && (
278
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
279
+ <Card className="w-full max-w-md">
280
+ <CardContent>
281
+ <h3 className="text-lg font-semibold text-white mb-2">Delete Eval Case</h3>
282
+ <p className="text-sm text-white/60 mb-6">
283
+ Are you sure you want to delete this eval case? This action cannot be undone.
284
+ </p>
285
+ <div className="flex justify-end gap-3">
286
+ <Button variant="ghost" onClick={() => setDeleteConfirm(null)}>
287
+ Cancel
288
+ </Button>
289
+ <Button variant="danger" onClick={() => handleDelete(deleteConfirm)}>
290
+ Delete
291
+ </Button>
292
+ </div>
293
+ </CardContent>
294
+ </Card>
295
+ </div>
296
+ )}
297
+ </div>
298
+ )
299
+ }
300
+
301
+ // ─── Eval Case Create/Edit Modal ───
302
+
303
+ function EvalCaseModal({
304
+ agentName,
305
+ evalCase,
306
+ onClose,
307
+ onSaved,
308
+ }: {
309
+ agentName: string
310
+ evalCase: any | null
311
+ onClose: () => void
312
+ onSaved: () => void
313
+ }) {
314
+ const isEdit = !!evalCase
315
+ const [question, setQuestion] = useState(evalCase?.question || '')
316
+ const [expectedTopics, setExpectedTopics] = useState(
317
+ evalCase?.expectedTopics?.join(', ') || ''
318
+ )
319
+ const [expectedNotTopics, setExpectedNotTopics] = useState(
320
+ evalCase?.expectedNotTopics?.join(', ') || ''
321
+ )
322
+ const [referenceAnswer, setReferenceAnswer] = useState(evalCase?.referenceAnswer || '')
323
+ const [category, setCategory] = useState(evalCase?.category || '')
324
+ const [tags, setTags] = useState(evalCase?.tags?.join(', ') || '')
325
+ const [saving, setSaving] = useState(false)
326
+ const [error, setError] = useState<string | null>(null)
327
+
328
+ function parseList(s: string): string[] {
329
+ return s
330
+ .split(',')
331
+ .map((t) => t.trim())
332
+ .filter(Boolean)
333
+ }
334
+
335
+ async function handleSave() {
336
+ if (!question.trim()) {
337
+ setError('Question is required')
338
+ return
339
+ }
340
+ setSaving(true)
341
+ setError(null)
342
+ try {
343
+ const data = {
344
+ question: question.trim(),
345
+ expectedTopics: expectedTopics ? parseList(expectedTopics) : undefined,
346
+ expectedNotTopics: expectedNotTopics ? parseList(expectedNotTopics) : undefined,
347
+ referenceAnswer: referenceAnswer || undefined,
348
+ category: category || undefined,
349
+ tags: tags ? parseList(tags) : undefined,
350
+ }
351
+ if (isEdit) {
352
+ await getClient().updateEvalCase(agentName, evalCase.id, data)
353
+ } else {
354
+ await getClient().createEvalCase(agentName, data)
355
+ }
356
+ onSaved()
357
+ } catch (e) {
358
+ setError(e instanceof Error ? e.message : 'Failed to save eval case')
359
+ } finally {
360
+ setSaving(false)
361
+ }
362
+ }
363
+
364
+ return (
365
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
366
+ <Card className="w-full max-w-lg max-h-[90vh] overflow-auto">
367
+ <CardContent>
368
+ <div className="flex items-center justify-between mb-6">
369
+ <h2 className="text-lg font-semibold text-white">
370
+ {isEdit ? 'Edit Eval Case' : 'Add Eval Case'}
371
+ </h2>
372
+ <button onClick={onClose} className="text-white/40 hover:text-white">
373
+ <X className="h-5 w-5" />
374
+ </button>
375
+ </div>
376
+
377
+ <div className="space-y-4">
378
+ <div className="space-y-1.5">
379
+ <label className="block text-sm font-medium text-white/70">Question</label>
380
+ <textarea
381
+ value={question}
382
+ onChange={(e) => setQuestion(e.target.value)}
383
+ rows={3}
384
+ placeholder="What question should the agent answer?"
385
+ className="flex w-full rounded-xl border px-3 py-2 text-sm bg-white/5 border-white/10 text-white placeholder:text-white/40 focus-visible:outline-none focus-visible:border-indigo-500/50 resize-none"
386
+ />
387
+ </div>
388
+ <Input
389
+ label="Expected Topics (comma-separated)"
390
+ placeholder="e.g. pricing, features, support"
391
+ value={expectedTopics}
392
+ onChange={(e) => setExpectedTopics(e.target.value)}
393
+ />
394
+ <Input
395
+ label="Should NOT Mention (comma-separated)"
396
+ placeholder="e.g. competitor names, internal details"
397
+ value={expectedNotTopics}
398
+ onChange={(e) => setExpectedNotTopics(e.target.value)}
399
+ />
400
+ <div className="space-y-1.5">
401
+ <label className="block text-sm font-medium text-white/70">
402
+ Reference Answer (optional)
403
+ </label>
404
+ <textarea
405
+ value={referenceAnswer}
406
+ onChange={(e) => setReferenceAnswer(e.target.value)}
407
+ rows={3}
408
+ placeholder="The ideal answer for comparison..."
409
+ className="flex w-full rounded-xl border px-3 py-2 text-sm bg-white/5 border-white/10 text-white placeholder:text-white/40 focus-visible:outline-none focus-visible:border-indigo-500/50 resize-none"
410
+ />
411
+ </div>
412
+ <Input
413
+ label="Category"
414
+ placeholder="e.g. accuracy, safety, edge_case"
415
+ value={category}
416
+ onChange={(e) => setCategory(e.target.value)}
417
+ />
418
+ <Input
419
+ label="Tags (comma-separated)"
420
+ placeholder="e.g. regression, critical"
421
+ value={tags}
422
+ onChange={(e) => setTags(e.target.value)}
423
+ />
424
+
425
+ {error && <p className="text-sm text-red-400">{error}</p>}
426
+
427
+ <div className="flex justify-end gap-3 pt-2">
428
+ <Button variant="ghost" onClick={onClose}>
429
+ Cancel
430
+ </Button>
431
+ <Button onClick={handleSave} disabled={saving}>
432
+ {saving ? 'Saving...' : isEdit ? 'Update' : 'Create'}
433
+ </Button>
434
+ </div>
435
+ </div>
436
+ </CardContent>
437
+ </Card>
438
+ </div>
439
+ )
440
+ }
441
+
442
+ export default function EvalsPage() {
443
+ return (
444
+ <Suspense fallback={<ShimmerBlock height={200} />}>
445
+ <EvalsContent />
446
+ </Suspense>
447
+ )
448
+ }
@@ -0,0 +1,202 @@
1
+ 'use client'
2
+
3
+ import { Suspense, useState, useRef, useCallback } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { useAgentFiles } from '@/lib/hooks'
7
+ import { getClient } from '@/lib/client'
8
+ import { Card, CardContent } from '@/components/ui/card'
9
+ import { Button } from '@/components/ui/button'
10
+ import { EmptyState } from '@/components/ui/empty-state'
11
+ import { ShimmerBlock } from '@/components/ui/shimmer'
12
+ import { formatRelativeTime } from '@/lib/utils'
13
+ import {
14
+ ArrowLeft,
15
+ BookOpen,
16
+ Upload,
17
+ Trash2,
18
+ FileText,
19
+ ChevronDown,
20
+ ChevronRight,
21
+ X,
22
+ Loader2,
23
+ } from 'lucide-react'
24
+
25
+ function KnowledgeContent() {
26
+ const searchParams = useSearchParams()
27
+ const name = searchParams.get('name')
28
+ const { files, loading, refresh } = useAgentFiles(name)
29
+ const [expandedFile, setExpandedFile] = useState<string | null>(null)
30
+ const [deleting, setDeleting] = useState<string | null>(null)
31
+ const [uploading, setUploading] = useState(false)
32
+ const [error, setError] = useState<string | null>(null)
33
+ const fileInputRef = useRef<HTMLInputElement>(null)
34
+
35
+ const handleUpload = useCallback(async (fileList: FileList) => {
36
+ if (!name) return
37
+ setUploading(true)
38
+ setError(null)
39
+ try {
40
+ const filesToUpload: Array<{ path: string; content: string }> = []
41
+ for (const file of Array.from(fileList)) {
42
+ const text = await file.text()
43
+ filesToUpload.push({ path: file.name, content: text })
44
+ }
45
+ await getClient().uploadAgentFiles(name, filesToUpload)
46
+ refresh()
47
+ } catch (e) {
48
+ setError(e instanceof Error ? e.message : 'Failed to upload files')
49
+ } finally {
50
+ setUploading(false)
51
+ }
52
+ }, [name, refresh])
53
+
54
+ async function handleDelete(filePath: string) {
55
+ if (!name) return
56
+ setDeleting(filePath)
57
+ setError(null)
58
+ try {
59
+ await getClient().deleteAgentFile(name, filePath)
60
+ setExpandedFile(null)
61
+ refresh()
62
+ } catch (e) {
63
+ setError(e instanceof Error ? e.message : 'Failed to delete file')
64
+ } finally {
65
+ setDeleting(null)
66
+ }
67
+ }
68
+
69
+ if (!name) {
70
+ return (
71
+ <div className="text-center py-16">
72
+ <p className="text-white/50">No agent name specified.</p>
73
+ <Link href="/agents" className="text-indigo-400 hover:text-indigo-300 text-sm mt-2 inline-block">
74
+ Back to agents
75
+ </Link>
76
+ </div>
77
+ )
78
+ }
79
+
80
+ return (
81
+ <div className="space-y-6">
82
+ {/* Back link */}
83
+ <Link
84
+ href={`/agents/detail?name=${encodeURIComponent(name)}`}
85
+ className="inline-flex items-center gap-1.5 text-sm text-white/50 hover:text-white transition-colors"
86
+ >
87
+ <ArrowLeft className="h-4 w-4" />
88
+ Back to {name}
89
+ </Link>
90
+
91
+ <div className="flex items-center justify-between">
92
+ <div>
93
+ <h1 className="text-2xl font-bold text-white">Knowledge Base</h1>
94
+ <p className="mt-1 text-sm text-white/50">
95
+ Files for <span className="text-white/70">{name}</span>
96
+ </p>
97
+ </div>
98
+ <Button onClick={() => fileInputRef.current?.click()} disabled={uploading}>
99
+ {uploading ? (
100
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
101
+ ) : (
102
+ <Upload className="h-4 w-4 mr-2" />
103
+ )}
104
+ {uploading ? 'Uploading...' : 'Upload Files'}
105
+ </Button>
106
+ <input
107
+ ref={fileInputRef}
108
+ type="file"
109
+ multiple
110
+ className="hidden"
111
+ onChange={(e) => {
112
+ if (e.target.files && e.target.files.length > 0) {
113
+ handleUpload(e.target.files)
114
+ e.target.value = ''
115
+ }
116
+ }}
117
+ />
118
+ </div>
119
+
120
+ {error && (
121
+ <div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-4 py-2">
122
+ {error}
123
+ </div>
124
+ )}
125
+
126
+ {loading ? (
127
+ <div className="space-y-3">
128
+ {[1, 2, 3].map((i) => (
129
+ <ShimmerBlock key={i} height={60} />
130
+ ))}
131
+ </div>
132
+ ) : files.length === 0 ? (
133
+ <EmptyState
134
+ icon={<BookOpen className="h-12 w-12" />}
135
+ title="No files yet"
136
+ description="Upload knowledge base files for your agent. These files will be available in the agent's working directory."
137
+ action={
138
+ <Button onClick={() => fileInputRef.current?.click()}>
139
+ <Upload className="h-4 w-4 mr-2" />
140
+ Upload Files
141
+ </Button>
142
+ }
143
+ />
144
+ ) : (
145
+ <div className="space-y-2">
146
+ {files.map((file: any) => {
147
+ const filePath = typeof file === 'string' ? file : file.path || file.name
148
+ const isExpanded = expandedFile === filePath
149
+ return (
150
+ <Card key={filePath}>
151
+ <CardContent className="!py-3">
152
+ <div className="flex items-center justify-between">
153
+ <button
154
+ onClick={() => setExpandedFile(isExpanded ? null : filePath)}
155
+ className="flex items-center gap-2 min-w-0 flex-1 text-left"
156
+ >
157
+ {isExpanded ? (
158
+ <ChevronDown className="h-4 w-4 text-white/40 flex-shrink-0" />
159
+ ) : (
160
+ <ChevronRight className="h-4 w-4 text-white/40 flex-shrink-0" />
161
+ )}
162
+ <FileText className="h-4 w-4 text-indigo-400 flex-shrink-0" />
163
+ <span className="text-sm text-white truncate">{filePath}</span>
164
+ </button>
165
+ <Button
166
+ variant="ghost"
167
+ size="sm"
168
+ onClick={() => handleDelete(filePath)}
169
+ disabled={deleting === filePath}
170
+ className="text-white/30 hover:text-red-400 flex-shrink-0 ml-2"
171
+ >
172
+ {deleting === filePath ? (
173
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
174
+ ) : (
175
+ <Trash2 className="h-3.5 w-3.5" />
176
+ )}
177
+ </Button>
178
+ </div>
179
+ {isExpanded && typeof file === 'object' && file.content && (
180
+ <div className="mt-3 pt-3 border-t border-white/5">
181
+ <pre className="text-xs text-white/60 bg-black/20 rounded-lg p-3 overflow-x-auto max-h-64 overflow-y-auto whitespace-pre-wrap">
182
+ {file.content}
183
+ </pre>
184
+ </div>
185
+ )}
186
+ </CardContent>
187
+ </Card>
188
+ )
189
+ })}
190
+ </div>
191
+ )}
192
+ </div>
193
+ )
194
+ }
195
+
196
+ export default function KnowledgePage() {
197
+ return (
198
+ <Suspense fallback={<ShimmerBlock height={200} />}>
199
+ <KnowledgeContent />
200
+ </Suspense>
201
+ )
202
+ }
@@ -147,9 +147,11 @@ function AgentCard({
147
147
  <CardContent>
148
148
  <div className="flex items-start justify-between">
149
149
  <div className="min-w-0 flex-1">
150
- <h3 className="text-sm font-semibold text-white truncate">
151
- {agent.name}
152
- </h3>
150
+ <Link href={`/agents/detail?name=${encodeURIComponent(agent.name)}`}>
151
+ <h3 className="text-sm font-semibold text-white truncate hover:text-indigo-400 transition-colors cursor-pointer">
152
+ {agent.name}
153
+ </h3>
154
+ </Link>
153
155
  {agent.description && (
154
156
  <p className="text-xs text-white/40 mt-1 line-clamp-2">
155
157
  {agent.description}