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 | null; created_at?: string | null; }; type PaginatedResponse = { 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(null); const [runs, setRuns] = useState([]); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const [pendingMessageClear, setPendingMessageClear] = useState(false); const [lastKnownStep, setLastKnownStep] = useState(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>("/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( `/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, }; }