Files
vkv/resources/js/hooks/useRoundEvaluationRun.ts
Zdeněk Burda 41e3ce6f25 Initial commit
2026-01-09 21:26:40 +01:00

327 lines
9.3 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import axios from "axios";
import { useTranslation } from "react-i18next";
export type EvaluationRun = {
id: number;
round_id: number;
name?: string | null;
rules_version?: string | null;
result_type?: string | null;
status?: string | null;
current_step?: string | null;
created_at?: string | null;
started_at?: string | null;
finished_at?: string | null;
progress_total?: number | null;
progress_done?: number | null;
};
export type EvaluationRunEvent = {
id: number;
level: string;
message: string;
context?: Record<string, unknown> | null;
created_at?: string | null;
};
type PaginatedResponse<T> = {
data: T[];
current_page: number;
last_page: number;
total: number;
};
const isActiveStatus = (status?: string | null) =>
status === "PENDING" ||
status === "RUNNING" ||
status === "WAITING_REVIEW_INPUT" ||
status === "WAITING_REVIEW_MATCH" ||
status === "WAITING_REVIEW_SCORE";
const isOfficialRun = (run: EvaluationRun | null) =>
!!run && run.rules_version !== "CLAIMED";
export const steps = [
{ key: "prepare", label: "Prepare" },
{ key: "parse_logs", label: "Parse" },
{ key: "build_working_set", label: "Working set" },
{ key: "waiting_review_input", label: "Review input" },
{ key: "match", label: "Match" },
{ key: "waiting_review_match", label: "Review match" },
{ key: "score", label: "Score" },
{ key: "aggregate", label: "Aggregate" },
{ key: "waiting_review_score", label: "Review score" },
{ key: "finalize", label: "Finalize" },
];
export default function useRoundEvaluationRun(roundId: number | null) {
const { i18n } = useTranslation();
const [run, setRun] = useState<EvaluationRun | null>(null);
const [runs, setRuns] = useState<EvaluationRun[]>([]);
const [events, setEvents] = useState<EvaluationRunEvent[]>([]);
const [loading, setLoading] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [pendingMessageClear, setPendingMessageClear] = useState(false);
const [lastKnownStep, setLastKnownStep] = useState<string | null>(null);
const [hasLoaded, setHasLoaded] = useState(false);
const load = async (opts?: { silent?: boolean }) => {
if (!roundId) return;
try {
if (!opts?.silent) {
setLoading(true);
}
if (!opts?.silent) {
setError(null);
}
const res = await axios.get<PaginatedResponse<EvaluationRun>>("/api/evaluation-runs", {
headers: { Accept: "application/json" },
params: { round_id: roundId, per_page: 20 },
withCredentials: true,
});
const officialRuns = res.data.data.filter((item) => item.rules_version !== "CLAIMED");
const limitedRuns = officialRuns.slice(0, 3);
const latest = limitedRuns[0] ?? null;
setRuns(limitedRuns);
setRun(latest);
setHasLoaded(true);
if (pendingMessageClear) {
setMessage(null);
setPendingMessageClear(false);
}
} catch (e: any) {
if (!opts?.silent) {
const msg = e?.response?.data?.message || "Nepodařilo se načíst stav vyhodnocení.";
setError(msg);
}
} finally {
if (!opts?.silent) {
setLoading(false);
}
}
};
const fetchEvents = async (runId: number) => {
try {
const res = await axios.get<EvaluationRunEvent[]>(
`/api/evaluation-runs/${runId}/events`,
{
headers: { Accept: "application/json" },
params: { limit: 8, min_level: "warning" },
withCredentials: true,
}
);
setEvents(res.data);
} catch {
setEvents([]);
}
};
useEffect(() => {
load();
}, [roundId]);
useEffect(() => {
if (!run || !isOfficialRun(run)) return;
fetchEvents(run.id);
}, [run?.id]);
const canStart = useMemo(() => {
if (!roundId) return false;
if (!run) return true;
return !isActiveStatus(run.status);
}, [roundId, run]);
const canResume =
run?.status === "WAITING_REVIEW_INPUT" ||
run?.status === "WAITING_REVIEW_MATCH" ||
run?.status === "WAITING_REVIEW_SCORE";
const canCancel = run && isActiveStatus(run.status);
const shouldPollRun =
run && isOfficialRun(run) && (run.status === "RUNNING" || run.status === "PENDING");
const shouldPollEvents = shouldPollRun;
const currentStepIndex = run
? steps.findIndex((step) => step.key === (run.current_step ?? lastKnownStep))
: -1;
const isSucceeded = run?.status === "SUCCEEDED";
const stepProgressPercent =
run && run.progress_total && run.progress_total > 0
? Math.round((run.progress_done / run.progress_total) * 100)
: null;
useEffect(() => {
if (!run || !shouldPollEvents) return;
const interval = window.setInterval(() => {
fetchEvents(run.id);
}, 1000);
return () => window.clearInterval(interval);
}, [run?.id, shouldPollEvents]);
useEffect(() => {
if (!run || !shouldPollRun) return;
const interval = window.setInterval(() => {
load({ silent: true });
}, 1000);
return () => window.clearInterval(interval);
}, [run?.id, shouldPollRun]);
useEffect(() => {
if (run?.current_step) {
setLastKnownStep(run.current_step);
}
}, [run?.current_step]);
const formatEventTime = (value?: string | null) => {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
const locale = i18n.language?.startsWith("cs") ? "cs-CZ" : "en-US";
return new Intl.DateTimeFormat(locale, {
dateStyle: "medium",
timeStyle: "short",
}).format(date);
};
const handleStart = async () => {
if (!roundId) return;
try {
setActionLoading(true);
setMessage(null);
setError(null);
const res = await axios.post(`/api/rounds/${roundId}/evaluation-runs/start`, null, {
headers: { Accept: "application/json" },
withCredentials: true,
withXSRFToken: true,
});
setRun(res.data);
setMessage("Vyhodnocení bylo spuštěno.");
setPendingMessageClear(true);
} catch (e: any) {
const msg = e?.response?.data?.message || "Nepodařilo se spustit vyhodnocení.";
setError(msg);
} finally {
setActionLoading(false);
}
};
const handleStartIncremental = async () => {
if (!roundId) return;
try {
setActionLoading(true);
setMessage(null);
setError(null);
const res = await axios.post(`/api/rounds/${roundId}/evaluation-runs/start-incremental`, null, {
headers: { Accept: "application/json" },
withCredentials: true,
withXSRFToken: true,
});
setRun(res.data);
setMessage("Vyhodnocení bylo spuštěno znovu.");
setPendingMessageClear(true);
} catch (e: any) {
const msg = e?.response?.data?.message || "Nepodařilo se spustit vyhodnocení znovu.";
setError(msg);
} finally {
setActionLoading(false);
}
};
const handleResume = async () => {
if (!run) return;
try {
setActionLoading(true);
setMessage(null);
setError(null);
await axios.post(`/api/evaluation-runs/${run.id}/resume`, null, {
headers: { Accept: "application/json" },
withCredentials: true,
withXSRFToken: true,
});
setMessage("Pokračování bylo spuštěno.");
setPendingMessageClear(true);
await load();
} catch (e: any) {
const msg = e?.response?.data?.message || "Nepodařilo se pokračovat.";
setError(msg);
} finally {
setActionLoading(false);
}
};
const handleCancel = async () => {
if (!run) return;
try {
setActionLoading(true);
setMessage(null);
setError(null);
await axios.post(`/api/evaluation-runs/${run.id}/cancel`, null, {
headers: { Accept: "application/json" },
withCredentials: true,
withXSRFToken: true,
});
setMessage("Vyhodnocení bylo zrušeno.");
await load();
} catch (e: any) {
const msg = e?.response?.data?.message || "Nepodařilo se zrušit běh.";
setError(msg);
} finally {
setActionLoading(false);
}
};
const handleSetResultType = async (resultType: "PRELIMINARY" | "FINAL" | "TEST") => {
if (!run) return;
try {
setActionLoading(true);
setMessage(null);
setError(null);
const res = await axios.post(
`/api/evaluation-runs/${run.id}/result-type`,
{ result_type: resultType },
{
headers: { Accept: "application/json" },
withCredentials: true,
withXSRFToken: true,
}
);
setRun(res.data);
setMessage("Typ výsledků byl uložen.");
setPendingMessageClear(true);
} catch (e: any) {
const msg = e?.response?.data?.message || "Nepodařilo se uložit typ výsledků.";
setError(msg);
} finally {
setActionLoading(false);
}
};
return {
run,
runs,
events,
loading,
actionLoading,
message,
error,
hasLoaded,
canStart,
canResume,
canCancel,
shouldPollRun,
isOfficialRun: isOfficialRun(run),
currentStepIndex,
isSucceeded,
stepProgressPercent,
formatEventTime,
handleStart,
handleStartIncremental,
handleResume,
handleCancel,
handleSetResultType,
};
}