@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.
- package/app/agents/detail/page.tsx +182 -0
- package/app/agents/eval-compare/page.tsx +445 -0
- package/app/agents/eval-run/page.tsx +331 -0
- package/app/agents/eval-runs/page.tsx +376 -0
- package/app/agents/evals/page.tsx +448 -0
- package/app/agents/knowledge/page.tsx +202 -0
- package/app/agents/page.tsx +5 -3
- package/app/agents/versions/page.tsx +280 -0
- package/lib/hooks.ts +120 -0
- package/out/404/index.html +1 -1
- package/out/404.html +1 -1
- package/out/_next/static/chunks/839-186b95b4d095e127.js +1 -0
- package/out/_next/static/chunks/90-8e47c31ab931867f.js +1 -0
- package/out/_next/static/chunks/{929-6faf1adeb65ee383.js → 929-b631fe082fe4e852.js} +1 -1
- package/out/_next/static/chunks/app/agents/detail/page-7427483b9c74ad63.js +1 -0
- package/out/_next/static/chunks/app/agents/eval-compare/page-bc4e8051ba07e56f.js +1 -0
- package/out/_next/static/chunks/app/agents/eval-run/page-a38dac74d1b3787d.js +1 -0
- package/out/_next/static/chunks/app/agents/eval-runs/page-af2fb33c0fce7934.js +1 -0
- package/out/_next/static/chunks/app/agents/evals/page-6bdc4b839a7a8eda.js +1 -0
- package/out/_next/static/chunks/app/agents/knowledge/page-0e02b14bfa2a6d04.js +1 -0
- package/out/_next/static/chunks/app/agents/page-99f179eb7c41ebd4.js +1 -0
- package/out/_next/static/chunks/app/agents/versions/page-c482d9bad8f35df6.js +1 -0
- package/out/_next/static/chunks/app/analytics/page-ca5d8c60e62118ed.js +1 -0
- package/out/_next/static/chunks/app/layout-b06d1caafc026d0c.js +1 -0
- package/out/_next/static/chunks/app/logs/page-1a7df17a605f36d3.js +1 -0
- package/out/_next/static/chunks/app/page-9e02cb0e8897ab5d.js +1 -0
- package/out/_next/static/chunks/app/playground/{page-10d3461f118bfb21.js → page-cb17c2ffaeb31b4e.js} +1 -1
- package/out/_next/static/chunks/app/queue/page-6013b93817822c75.js +1 -0
- package/out/_next/static/chunks/app/sessions/page-add67d96ab66b690.js +1 -0
- package/out/_next/static/chunks/app/settings/credentials/page-ffe97ffb2f60229d.js +1 -0
- package/out/_next/static/css/ab505eeeff3f7df5.css +1 -0
- package/out/_next/static/sXYgh3eUKXRKt1T_1T3tk/_buildManifest.js +1 -0
- package/out/agents/detail/index.html +1 -0
- package/out/agents/detail/index.txt +22 -0
- package/out/agents/eval-compare/index.html +1 -0
- package/out/agents/eval-compare/index.txt +22 -0
- package/out/agents/eval-run/index.html +1 -0
- package/out/agents/eval-run/index.txt +22 -0
- package/out/agents/eval-runs/index.html +1 -0
- package/out/agents/eval-runs/index.txt +22 -0
- package/out/agents/evals/index.html +1 -0
- package/out/agents/evals/index.txt +22 -0
- package/out/agents/index.html +1 -1
- package/out/agents/index.txt +6 -6
- package/out/agents/knowledge/index.html +1 -0
- package/out/agents/knowledge/index.txt +22 -0
- package/out/agents/versions/index.html +1 -0
- package/out/agents/versions/index.txt +22 -0
- package/out/analytics/index.html +1 -1
- package/out/analytics/index.txt +6 -6
- package/out/index.html +1 -1
- package/out/index.txt +6 -6
- package/out/logs/index.html +1 -1
- package/out/logs/index.txt +6 -6
- package/out/playground/index.html +1 -1
- package/out/playground/index.txt +6 -6
- package/out/queue/index.html +1 -1
- package/out/queue/index.txt +6 -6
- package/out/sessions/index.html +1 -1
- package/out/sessions/index.txt +6 -6
- package/out/settings/api-keys/index.html +1 -1
- package/out/settings/api-keys/index.txt +6 -6
- package/out/settings/credentials/index.html +1 -1
- package/out/settings/credentials/index.txt +6 -6
- package/package.json +4 -4
- package/out/_next/static/_fEfzU87y-4u457akpPDC/_buildManifest.js +0 -1
- package/out/_next/static/chunks/322-bab4df5c5188e993.js +0 -1
- package/out/_next/static/chunks/432-11ec8af7ccfbd019.js +0 -1
- package/out/_next/static/chunks/app/agents/page-5f872b5fa12d7854.js +0 -1
- package/out/_next/static/chunks/app/analytics/page-bb296f848e25a94f.js +0 -1
- package/out/_next/static/chunks/app/layout-f5d1d76b525135c7.js +0 -1
- package/out/_next/static/chunks/app/logs/page-5165b556d13654ae.js +0 -1
- package/out/_next/static/chunks/app/page-d1e6d7bff1216f08.js +0 -1
- package/out/_next/static/chunks/app/queue/page-50142f2cfb3664e7.js +0 -1
- package/out/_next/static/chunks/app/sessions/page-7f55a0a4ba0be458.js +0 -1
- package/out/_next/static/chunks/app/settings/credentials/page-deb5556bfe57b8b9.js +0 -1
- package/out/_next/static/css/15bfa5d891bcf58c.css +0 -1
- /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
|
+
}
|
package/app/agents/page.tsx
CHANGED
|
@@ -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
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
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}
|