feat: persist single evaluation history
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
LoaderCircleIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
RotateCcwIcon,
|
||||
SearchIcon,
|
||||
@@ -122,6 +123,8 @@ type EvaluationCase = {
|
||||
skill_name?: string;
|
||||
skill_version?: string;
|
||||
model?: string;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
};
|
||||
|
||||
type Experiment = {
|
||||
@@ -213,6 +216,8 @@ function EvaluationWorkspace() {
|
||||
const [selectedDatasetId, setSelectedDatasetId] = useState("");
|
||||
const [selectedExperimentId, setSelectedExperimentId] = useState("");
|
||||
const [experiment, setExperiment] = useState<Experiment | null>(null);
|
||||
const [singleAnalyses, setSingleAnalyses] = useState<EvaluationCase[]>([]);
|
||||
const [selectedSingleAnalysisId, setSelectedSingleAnalysisId] = useState("");
|
||||
const [mapping, setMapping] = useState<DatasetMapping>({
|
||||
fields: {},
|
||||
label_map: {},
|
||||
@@ -233,6 +238,7 @@ function EvaluationWorkspace() {
|
||||
const uploadRef = useRef<HTMLInputElement>(null);
|
||||
const selectedDatasetIdRef = useRef("");
|
||||
const selectedExperimentIdRef = useRef("");
|
||||
const selectedSingleAnalysisIdRef = useRef("");
|
||||
|
||||
useEffect(() => {
|
||||
selectedDatasetIdRef.current = selectedDatasetId;
|
||||
@@ -242,6 +248,10 @@ function EvaluationWorkspace() {
|
||||
selectedExperimentIdRef.current = selectedExperimentId;
|
||||
}, [selectedExperimentId]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedSingleAnalysisIdRef.current = selectedSingleAnalysisId;
|
||||
}, [selectedSingleAnalysisId]);
|
||||
|
||||
const selectedDataset = useMemo(
|
||||
() => datasets.find((item) => item.id === selectedDatasetId) ?? null,
|
||||
[datasets, selectedDatasetId],
|
||||
@@ -264,12 +274,14 @@ function EvaluationWorkspace() {
|
||||
try {
|
||||
const [
|
||||
metadataResponse,
|
||||
analysisResponse,
|
||||
datasetResponse,
|
||||
experimentResponse,
|
||||
modelResponse,
|
||||
stateResponse,
|
||||
] = await Promise.all([
|
||||
fetch("/api/claw/evaluations/metadata", { cache: "no-store" }),
|
||||
fetch("/api/claw/evaluations/analyses", { cache: "no-store" }),
|
||||
fetch("/api/claw/evaluations/datasets", { cache: "no-store" }),
|
||||
fetch("/api/claw/evaluations/experiments", { cache: "no-store" }),
|
||||
fetch("/api/claw/models", { cache: "no-store" }),
|
||||
@@ -277,6 +289,8 @@ function EvaluationWorkspace() {
|
||||
]);
|
||||
const metadataPayload =
|
||||
await readJson<EvaluationMetadata>(metadataResponse);
|
||||
const analysisPayload =
|
||||
await readJson<EvaluationCase[]>(analysisResponse);
|
||||
const datasetPayload = await readJson<Dataset[]>(datasetResponse);
|
||||
const experimentPayload =
|
||||
await readJson<Experiment[]>(experimentResponse);
|
||||
@@ -287,11 +301,21 @@ function EvaluationWorkspace() {
|
||||
? ((await stateResponse.json()) as ClawState)
|
||||
: {};
|
||||
setMetadata(metadataPayload);
|
||||
setSingleAnalyses(analysisPayload);
|
||||
setDatasets(datasetPayload);
|
||||
setExperiments(experimentPayload);
|
||||
setSkillName(
|
||||
(current) => current || metadataPayload.default_skill || "label-master",
|
||||
);
|
||||
const nextSingleAnalysis =
|
||||
analysisPayload.find(
|
||||
(item) => item.id === selectedSingleAnalysisIdRef.current,
|
||||
) ?? analysisPayload[0];
|
||||
if (nextSingleAnalysis) {
|
||||
setSelectedSingleAnalysisId(nextSingleAnalysis.id);
|
||||
setSingleResult((current) => current ?? nextSingleAnalysis);
|
||||
setSingleQuery((current) => current || nextSingleAnalysis.query);
|
||||
}
|
||||
const nextModels = modelPayload.models ?? [];
|
||||
setModels(nextModels);
|
||||
setModel((current) => {
|
||||
@@ -454,7 +478,13 @@ function EvaluationWorkspace() {
|
||||
model: model || undefined,
|
||||
}),
|
||||
});
|
||||
setSingleResult(await readJson<EvaluationCase>(response));
|
||||
const payload = await readJson<EvaluationCase>(response);
|
||||
setSingleResult(payload);
|
||||
setSelectedSingleAnalysisId(payload.id);
|
||||
setSingleAnalyses((current) => [
|
||||
payload,
|
||||
...current.filter((item) => item.id !== payload.id),
|
||||
]);
|
||||
} catch (reason) {
|
||||
setError(messageFrom(reason));
|
||||
} finally {
|
||||
@@ -462,6 +492,35 @@ function EvaluationWorkspace() {
|
||||
}
|
||||
}
|
||||
|
||||
async function selectSingleAnalysis(analysisId: string) {
|
||||
setSelectedSingleAnalysisId(analysisId);
|
||||
setBusy("single-history");
|
||||
setError("");
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/claw/evaluations/analyses/${encodeURIComponent(analysisId)}`,
|
||||
{ cache: "no-store" },
|
||||
);
|
||||
const payload = await readJson<EvaluationCase>(response);
|
||||
setSingleResult(payload);
|
||||
setSingleQuery(payload.query);
|
||||
setSingleAnalyses((current) =>
|
||||
current.map((item) => (item.id === payload.id ? payload : item)),
|
||||
);
|
||||
} catch (reason) {
|
||||
setError(messageFrom(reason));
|
||||
} finally {
|
||||
setBusy("");
|
||||
}
|
||||
}
|
||||
|
||||
function newSingleAnalysis() {
|
||||
setSelectedSingleAnalysisId("");
|
||||
setSingleResult(null);
|
||||
setSingleQuery("");
|
||||
setError("");
|
||||
}
|
||||
|
||||
async function act(action: "start" | "pause" | "resume" | "cancel") {
|
||||
if (!experiment) return;
|
||||
setBusy(action);
|
||||
@@ -617,7 +676,12 @@ function EvaluationWorkspace() {
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{workspaceMode === "batch" ? (
|
||||
{workspaceMode === "single" ? (
|
||||
<Button size="sm" onClick={newSingleAnalysis}>
|
||||
<PlusIcon />
|
||||
新建单条分析
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@@ -627,7 +691,7 @@ function EvaluationWorkspace() {
|
||||
>
|
||||
新建批量评测
|
||||
</Button>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -640,13 +704,60 @@ function EvaluationWorkspace() {
|
||||
) : null}
|
||||
|
||||
{workspaceMode === "single" ? (
|
||||
<SingleQueryWorkspace
|
||||
query={singleQuery}
|
||||
setQuery={setSingleQuery}
|
||||
result={singleResult}
|
||||
onAnalyze={analyzeSingleQuery}
|
||||
busy={busy}
|
||||
/>
|
||||
<div className="grid min-h-[calc(100dvh-6.5rem)] lg:grid-cols-[260px_minmax(0,1fr)]">
|
||||
<aside className="border-r bg-muted/15 p-3">
|
||||
<div className="mb-2 flex items-center justify-between px-2">
|
||||
<span className="font-medium text-xs">单条分析记录</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{singleAnalyses.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{singleAnalyses.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.id}
|
||||
className={`w-full rounded-md px-2.5 py-2 text-left transition-colors ${
|
||||
selectedSingleAnalysisId === item.id
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "hover:bg-accent/60"
|
||||
}`}
|
||||
onClick={() => void selectSingleAnalysis(item.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusDot status={item.status} />
|
||||
<span className="min-w-0 flex-1 truncate font-medium text-sm">
|
||||
{item.query}
|
||||
</span>
|
||||
<ChevronRightIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-between gap-2 pl-5 text-muted-foreground text-xs">
|
||||
<span className="truncate">
|
||||
{item.prediction || item.skill_name || "分析失败"}
|
||||
</span>
|
||||
<span className="shrink-0">
|
||||
{formatTimestamp(item.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{!singleAnalyses.length ? (
|
||||
<div className="px-2 py-8 text-center text-muted-foreground text-xs">
|
||||
暂无单条分析
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
<section className="min-w-0">
|
||||
<SingleQueryWorkspace
|
||||
query={singleQuery}
|
||||
setQuery={setSingleQuery}
|
||||
result={singleResult}
|
||||
onAnalyze={analyzeSingleQuery}
|
||||
busy={busy}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid min-h-[calc(100dvh-6.5rem)] lg:grid-cols-[260px_minmax(0,1fr)]">
|
||||
<aside className="border-r bg-muted/15 p-3">
|
||||
@@ -1752,6 +1863,23 @@ function formatDuration(value: number | null) {
|
||||
return `${(value / 1000).toFixed(value < 10_000 ? 1 : 0)}s`;
|
||||
}
|
||||
|
||||
function formatTimestamp(value?: number) {
|
||||
if (!value) return "";
|
||||
const date = new Date(value * 1000);
|
||||
const now = new Date();
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
return date.toLocaleDateString("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function buildMappedPreview(
|
||||
rows: Array<Record<string, unknown>>,
|
||||
mapping: DatasetMapping,
|
||||
|
||||
Reference in New Issue
Block a user