126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
import { useState } from "react"
|
|
import { useApi, apiFetch } from "@/hooks/useApi"
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
type ExperimentSummary = {
|
|
id: string
|
|
name: string
|
|
status: string
|
|
run_count: number
|
|
}
|
|
|
|
type RunSummary = {
|
|
id: string
|
|
run_id: string
|
|
model_id: string
|
|
level: string
|
|
sample_id: string
|
|
repeat_index: number
|
|
status: string
|
|
retry_count: number
|
|
}
|
|
|
|
type RunDetail = {
|
|
id: string
|
|
run_id: string
|
|
model_id: string
|
|
level: string
|
|
sample_id: string
|
|
status: string
|
|
raw_output: string | null
|
|
latency_ms: number | null
|
|
}
|
|
|
|
export default function MonitorPage() {
|
|
const experimentsState = useApi<ExperimentSummary[]>("/experiments")
|
|
const [selectedExperimentId, setSelectedExperimentId] = useState<string | null>(null)
|
|
const runsState = useApi<RunSummary[]>(
|
|
selectedExperimentId ? `/experiments/${selectedExperimentId}/runs` : null,
|
|
[selectedExperimentId]
|
|
)
|
|
const [selectedRunId, setSelectedRunId] = useState<string | null>(null)
|
|
const runDetailState = useApi<RunDetail>(
|
|
selectedRunId ? `/experiments/${selectedExperimentId}/runs/${selectedRunId}` : null,
|
|
[selectedRunId, selectedExperimentId]
|
|
)
|
|
|
|
async function handleResume() {
|
|
if (!selectedExperimentId) return
|
|
await apiFetch(`/experiments/${selectedExperimentId}/run`, { method: "POST" })
|
|
alert("已触发断点续跑")
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h2 className="text-2xl font-semibold">实验监控</h2>
|
|
|
|
{experimentsState.status === "success" && (
|
|
<div className="flex gap-2 flex-wrap">
|
|
{experimentsState.data.map((e) => (
|
|
<Button
|
|
key={e.id}
|
|
variant={selectedExperimentId === e.id ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => {
|
|
setSelectedExperimentId(e.id)
|
|
setSelectedRunId(null)
|
|
}}
|
|
>
|
|
{e.name} ({e.run_count})
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{selectedExperimentId && (
|
|
<div className="flex justify-end">
|
|
<Button onClick={handleResume}>断点续跑</Button>
|
|
</div>
|
|
)}
|
|
|
|
{runsState.status === "success" && (
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-muted">
|
|
<tr>
|
|
<th className="px-4 py-2 text-left">模型</th>
|
|
<th className="px-4 py-2 text-left">级别</th>
|
|
<th className="px-4 py-2 text-left">重复</th>
|
|
<th className="px-4 py-2 text-left">状态</th>
|
|
<th className="px-4 py-2 text-left">重试</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{runsState.data.map((r) => (
|
|
<tr
|
|
key={r.id}
|
|
className={`cursor-pointer hover:bg-muted/50 ${selectedRunId === r.id ? "bg-muted" : ""}`}
|
|
onClick={() => setSelectedRunId(r.id)}
|
|
>
|
|
<td className="px-4 py-2">{r.model_id}</td>
|
|
<td className="px-4 py-2">{r.level}</td>
|
|
<td className="px-4 py-2">{r.repeat_index}</td>
|
|
<td className="px-4 py-2">{r.status}</td>
|
|
<td className="px-4 py-2">{r.retry_count}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{runDetailState.status === "success" && (
|
|
<div className="border rounded-lg p-4 space-y-2">
|
|
<h3 className="font-semibold">原始输出</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Latency: {runDetailState.data.latency_ms ?? "-"} ms
|
|
</p>
|
|
<pre className="bg-muted p-2 rounded text-xs overflow-auto max-h-96">
|
|
{runDetailState.data.raw_output ?? "无输出"}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|