import { useCallback, useEffect, useMemo, useState } from "react"; import { apiClient } from "../api/client"; import type { AutoRunCaseSummary, AutoRunDetailResponse, AutoRunDialogResponse, AutoRunDomainCoverage, AutoRunHistoryResponse, AutoRunSummary, ConnectionState, PromptState } from "../state/types"; import { JsonView } from "./JsonView"; import { PanelFrame } from "./PanelFrame"; interface AutoRunsHistoryPanelProps { connection: ConnectionState; prompts: PromptState; assistantPromptVersion: string; decompositionPromptVersion: string; onLog?: (message: string) => void; } type UseMockFilter = "any" | "true" | "false"; interface AutoRunsFilters { fromLocal: string; toLocal: string; target: string; mode: string; useMock: UseMockFilter; promptContains: string; limit: number; } const DEFAULT_FILTERS: AutoRunsFilters = { fromLocal: "", toLocal: "", target: "all", mode: "all", useMock: "any", promptContains: "", limit: 120 }; function dateToInputValue(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hour = String(date.getHours()).padStart(2, "0"); const minute = String(date.getMinutes()).padStart(2, "0"); return `${year}-${month}-${day}T${hour}:${minute}`; } function defaultFromDateValue(): string { const date = new Date(); date.setDate(date.getDate() - 14); return dateToInputValue(date); } function localInputToIso(value: string): string | undefined { if (!value.trim()) return undefined; const parsed = Date.parse(value); if (!Number.isFinite(parsed)) return undefined; return new Date(parsed).toISOString(); } function formatDateTime(iso: string): string { const parsed = Date.parse(iso); if (!Number.isFinite(parsed)) return iso; return new Date(parsed).toLocaleString("ru-RU"); } function toPercent(closed: number, total: number): number { if (total <= 0) return 0; return Math.max(0, Math.min(100, Number(((closed / total) * 100).toFixed(1)))); } function formatScore(value: number | null): string { if (typeof value !== "number") return "n/a"; return `${value.toFixed(1)}%`; } function formatShortTarget(value: string): string { if (value === "assistant_stage1") return "assistant/s1"; if (value === "assistant_stage2") return "assistant/s2"; if (value === "assistant_p0") return "assistant/p0"; return value; } function trendLabel(value: "up" | "down" | "flat"): string { if (value === "up") return "Рост"; if (value === "down") return "Регресс"; return "Без изменений"; } function getSelectedCase(cases: AutoRunCaseSummary[], caseId: string): AutoRunCaseSummary | null { return cases.find((item) => item.case_id === caseId) ?? null; } function renderCoverageRows(items: AutoRunDomainCoverage[]) { if (items.length === 0) { return

Покрытие доменов пока не сформировано.

; } return (
{items.map((item) => { const percent = toPercent(item.closed_cases, item.total_cases); return (
{item.domain} {item.closed_cases}/{item.total_cases} ({percent}%)
); })}
); } export function AutoRunsHistoryPanel({ connection, prompts, assistantPromptVersion, decompositionPromptVersion, onLog }: AutoRunsHistoryPanelProps) { const [filters, setFilters] = useState({ ...DEFAULT_FILTERS, fromLocal: defaultFromDateValue() }); const [history, setHistory] = useState(null); const [runDetail, setRunDetail] = useState(null); const [dialog, setDialog] = useState(null); const [selectedRunId, setSelectedRunId] = useState(""); const [selectedCaseId, setSelectedCaseId] = useState(""); const [historyBusy, setHistoryBusy] = useState(false); const [detailBusy, setDetailBusy] = useState(false); const [dialogBusy, setDialogBusy] = useState(false); const [errorText, setErrorText] = useState(""); const [showAssistantMode, setShowAssistantMode] = useState(true); const [showDecompositionMode, setShowDecompositionMode] = useState(true); const [showProgressMode, setShowProgressMode] = useState(true); const activeRunSummary: AutoRunSummary | null = history?.items.find((item) => item.run_id === selectedRunId) ?? null; const activeCase = runDetail ? getSelectedCase(runDetail.cases, selectedCaseId) : null; const log = useCallback( (message: string) => { onLog?.(`[autoruns] ${message}`); }, [onLog] ); const loadCaseDialog = useCallback( async (runId: string, caseId: string) => { setDialogBusy(true); try { const payload = await apiClient.loadAutoRunCaseDialog(runId, caseId); setDialog(payload); } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorText(`Диалог кейса: ${message}`); log(`Dialog load error for ${runId}/${caseId}: ${message}`); setDialog(null); } finally { setDialogBusy(false); } }, [log] ); const loadRunDetail = useCallback( async (runId: string, preferredCaseId?: string) => { setDetailBusy(true); try { const payload = await apiClient.loadAutoRunDetail(runId); setRunDetail(payload); const nextCaseId = (preferredCaseId && payload.cases.some((item) => item.case_id === preferredCaseId) ? preferredCaseId : "") || payload.cases[0]?.case_id || ""; setSelectedCaseId(nextCaseId); if (nextCaseId) { await loadCaseDialog(runId, nextCaseId); } else { setDialog(null); } } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorText(`Детализация прогона: ${message}`); log(`Run detail load error for ${runId}: ${message}`); setRunDetail(null); setDialog(null); } finally { setDetailBusy(false); } }, [loadCaseDialog, log] ); const loadHistory = useCallback( async (options?: { keepSelection?: boolean; preferredRunId?: string; preferredCaseId?: string }) => { setHistoryBusy(true); setErrorText(""); try { const payload = await apiClient.loadAutoRunsHistory({ from: localInputToIso(filters.fromLocal), to: localInputToIso(filters.toLocal), target: filters.target, mode: filters.mode, use_mock: filters.useMock, prompt_contains: filters.promptContains.trim() || undefined, limit: filters.limit }); setHistory(payload); const hasRuns = payload.items.length > 0; if (!hasRuns) { setSelectedRunId(""); setSelectedCaseId(""); setRunDetail(null); setDialog(null); return; } const keepSelection = options?.keepSelection ?? true; const preferredRunId = options?.preferredRunId ?? ""; const preferredCaseId = options?.preferredCaseId ?? ""; const nextRunId = keepSelection && preferredRunId && payload.items.some((item) => item.run_id === preferredRunId) ? preferredRunId : payload.items[0].run_id; setSelectedRunId(nextRunId); await loadRunDetail(nextRunId, keepSelection ? preferredCaseId : undefined); } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorText(`История прогонов: ${message}`); log(`History load error: ${message}`); } finally { setHistoryBusy(false); } }, [filters.fromLocal, filters.limit, filters.mode, filters.promptContains, filters.target, filters.toLocal, filters.useMock, loadRunDetail, log] ); useEffect(() => { void loadHistory({ keepSelection: false }); }, [loadHistory]); const dynamicColumns = useMemo(() => { const columns = ["minmax(290px, 340px)", "minmax(300px, 360px)", "minmax(420px, 1fr)"]; if (showAssistantMode) columns.push("minmax(280px, 320px)"); if (showDecompositionMode) columns.push("minmax(280px, 320px)"); if (showProgressMode) columns.push("minmax(280px, 320px)"); return columns.join(" "); }, [showAssistantMode, showDecompositionMode, showProgressMode]); return (
} >

Настройки выборки

{(history?.available.prompt_versions ?? []).map((item) => (

Контур генерации

Провайдер: {connection.llmProvider}
Модель: {connection.model || "n/a"}
Prompt assistant: {assistantPromptVersion}
Prompt decomposition: {decompositionPromptVersion}
Дублирование главного промпта (read-only)