Initial commit
This commit is contained in:
326
resources/js/hooks/useRoundEvaluationRun.ts
Normal file
326
resources/js/hooks/useRoundEvaluationRun.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user