АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия
This commit is contained in:
@@ -0,0 +1,664 @@
|
||||
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 <p className="muted">Покрытие доменов пока не сформировано.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="autoruns-coverage-list">
|
||||
{items.map((item) => {
|
||||
const percent = toPercent(item.closed_cases, item.total_cases);
|
||||
return (
|
||||
<div key={item.domain} className="autoruns-coverage-item">
|
||||
<div className="autoruns-coverage-head">
|
||||
<strong>{item.domain}</strong>
|
||||
<span>
|
||||
{item.closed_cases}/{item.total_cases} ({percent}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="autoruns-coverage-bar">
|
||||
<div style={{ width: `${percent}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AutoRunsHistoryPanel({
|
||||
connection,
|
||||
prompts,
|
||||
assistantPromptVersion,
|
||||
decompositionPromptVersion,
|
||||
onLog
|
||||
}: AutoRunsHistoryPanelProps) {
|
||||
const [filters, setFilters] = useState<AutoRunsFilters>({
|
||||
...DEFAULT_FILTERS,
|
||||
fromLocal: defaultFromDateValue()
|
||||
});
|
||||
const [history, setHistory] = useState<AutoRunHistoryResponse | null>(null);
|
||||
const [runDetail, setRunDetail] = useState<AutoRunDetailResponse | null>(null);
|
||||
const [dialog, setDialog] = useState<AutoRunDialogResponse | null>(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 (
|
||||
<PanelFrame
|
||||
title="История автопрогонов"
|
||||
subtitle="Центральный экран диагностики: фильтры, список прогонов, диалог по кейсу, режимы ассистента/декомпозиции и тренд качества."
|
||||
actions={
|
||||
<div className="assistant-panel-actions">
|
||||
<button type="button" className={showAssistantMode ? "tab active" : "tab"} onClick={() => setShowAssistantMode((prev) => !prev)}>
|
||||
Режим ассистента
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showDecompositionMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowDecompositionMode((prev) => !prev)}
|
||||
>
|
||||
Режим декомпозиции
|
||||
</button>
|
||||
<button type="button" className={showProgressMode ? "tab active" : "tab"} onClick={() => setShowProgressMode((prev) => !prev)}>
|
||||
Прогресс/регресс
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="autoruns-columns" style={{ gridTemplateColumns: dynamicColumns }}>
|
||||
<section className="autoruns-col">
|
||||
<h3>Настройки выборки</h3>
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Дата с
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={filters.fromLocal}
|
||||
onChange={(event) => setFilters((prev) => ({ ...prev, fromLocal: event.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Дата по
|
||||
<input type="datetime-local" value={filters.toLocal} onChange={(event) => setFilters((prev) => ({ ...prev, toLocal: event.target.value }))} />
|
||||
</label>
|
||||
<label>
|
||||
Целевой контур
|
||||
<select value={filters.target} onChange={(event) => setFilters((prev) => ({ ...prev, target: event.target.value }))}>
|
||||
<option value="all">all</option>
|
||||
{(history?.available.targets ?? []).map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Режим
|
||||
<select value={filters.mode} onChange={(event) => setFilters((prev) => ({ ...prev, mode: event.target.value }))}>
|
||||
<option value="all">all</option>
|
||||
{(history?.available.modes ?? []).map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
use_mock
|
||||
<select value={filters.useMock} onChange={(event) => setFilters((prev) => ({ ...prev, useMock: event.target.value as UseMockFilter }))}>
|
||||
<option value="any">any</option>
|
||||
<option value="true">true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Лимит
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={500}
|
||||
value={filters.limit}
|
||||
onChange={(event) => setFilters((prev) => ({ ...prev, limit: Number(event.target.value || 120) }))}
|
||||
/>
|
||||
</label>
|
||||
<label className="full-width">
|
||||
Версия промпта содержит
|
||||
<input
|
||||
value={filters.promptContains}
|
||||
onChange={(event) => setFilters((prev) => ({ ...prev, promptContains: event.target.value }))}
|
||||
placeholder="normalizer_v2_0_2 / address_query_runtime_v1"
|
||||
list="autoruns-prompt-versions"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<datalist id="autoruns-prompt-versions">
|
||||
{(history?.available.prompt_versions ?? []).map((item) => (
|
||||
<option key={item} value={item} />
|
||||
))}
|
||||
</datalist>
|
||||
<div className="button-row">
|
||||
<button type="button" disabled={historyBusy} onClick={() => void loadHistory({ keepSelection: false })}>
|
||||
{historyBusy ? "Обновляю..." : "Применить"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tab"
|
||||
onClick={() => {
|
||||
setFilters({
|
||||
...DEFAULT_FILTERS,
|
||||
fromLocal: defaultFromDateValue()
|
||||
});
|
||||
setErrorText("");
|
||||
}}
|
||||
>
|
||||
Сбросить фильтры
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h4>Контур генерации</h4>
|
||||
<div className="autoruns-meta-list">
|
||||
<div>
|
||||
<span>Провайдер:</span>
|
||||
<strong>{connection.llmProvider}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Модель:</span>
|
||||
<strong>{connection.model || "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Prompt assistant:</span>
|
||||
<strong>{assistantPromptVersion}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Prompt decomposition:</span>
|
||||
<strong>{decompositionPromptVersion}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details className="autoruns-prompt-details">
|
||||
<summary>Дублирование главного промпта (read-only)</summary>
|
||||
<label>
|
||||
System
|
||||
<textarea readOnly value={prompts.systemPrompt} />
|
||||
</label>
|
||||
<label>
|
||||
Developer
|
||||
<textarea readOnly value={prompts.developerPrompt} />
|
||||
</label>
|
||||
<label>
|
||||
Domain
|
||||
<textarea readOnly value={prompts.domainPrompt} />
|
||||
</label>
|
||||
<label>
|
||||
Schema notes
|
||||
<textarea readOnly value={prompts.schemaNotes} />
|
||||
</label>
|
||||
<label>
|
||||
Few-shot
|
||||
<textarea readOnly value={prompts.fewShotExamples} />
|
||||
</label>
|
||||
</details>
|
||||
|
||||
{errorText ? <p className="error-text">{errorText}</p> : null}
|
||||
</section>
|
||||
|
||||
<section className="autoruns-col">
|
||||
<h3>Выдача прогонов</h3>
|
||||
<div className="autoruns-stats-grid">
|
||||
<div>
|
||||
<span>Всего</span>
|
||||
<strong>{history?.stats.runs_total ?? 0}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Средний score</span>
|
||||
<strong>{formatScore(history?.stats.avg_score_index ?? null)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Тренд</span>
|
||||
<strong>{history ? trendLabel(history.stats.trend) : "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Блокеры</span>
|
||||
<strong>{history?.stats.blocking_runs ?? 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="autoruns-run-list">
|
||||
{(history?.items ?? []).map((run) => (
|
||||
<button
|
||||
key={run.run_id}
|
||||
type="button"
|
||||
className={selectedRunId === run.run_id ? "autoruns-run-item selected" : "autoruns-run-item"}
|
||||
onClick={() => {
|
||||
setSelectedRunId(run.run_id);
|
||||
void loadRunDetail(run.run_id);
|
||||
}}
|
||||
>
|
||||
<div className="autoruns-run-head">
|
||||
<strong>{formatDateTime(run.run_timestamp)}</strong>
|
||||
<span>{formatShortTarget(run.eval_target)}</span>
|
||||
</div>
|
||||
<div className="autoruns-run-meta">{run.run_id}</div>
|
||||
<div className="autoruns-run-meta">
|
||||
mode={run.mode ?? "n/a"} | mock={String(run.use_mock)}
|
||||
</div>
|
||||
{run.llm_provider || run.model ? (
|
||||
<div className="autoruns-run-meta">
|
||||
llm={run.llm_provider ?? "n/a"} | model={run.model ?? "n/a"}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="autoruns-run-meta">prompt={run.prompt_version ?? "n/a"}</div>
|
||||
<div className="autoruns-run-foot">
|
||||
<span>score: {formatScore(run.score_index)}</span>
|
||||
<span>
|
||||
closed/open: {run.closed_cases}/{run.open_cases}
|
||||
</span>
|
||||
</div>
|
||||
<div className="autoruns-run-foot">
|
||||
<span>blocking: {run.blocking_failures}</span>
|
||||
<span>quality: {run.quality_failures}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{(history?.items.length ?? 0) === 0 ? <p className="muted">За выбранный диапазон прогонов нет.</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="autoruns-col">
|
||||
<h3>Диалог прогона</h3>
|
||||
<div className="autoruns-dialog-toolbar">
|
||||
<label>
|
||||
Прогон
|
||||
<select
|
||||
value={selectedRunId}
|
||||
onChange={(event) => {
|
||||
const nextRunId = event.target.value;
|
||||
setSelectedRunId(nextRunId);
|
||||
void loadRunDetail(nextRunId);
|
||||
}}
|
||||
>
|
||||
{(history?.items ?? []).map((item) => (
|
||||
<option key={item.run_id} value={item.run_id}>
|
||||
{formatDateTime(item.run_timestamp)} | {item.run_id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Кейc
|
||||
<select
|
||||
value={selectedCaseId}
|
||||
onChange={(event) => {
|
||||
const nextCaseId = event.target.value;
|
||||
setSelectedCaseId(nextCaseId);
|
||||
if (selectedRunId && nextCaseId) {
|
||||
void loadCaseDialog(selectedRunId, nextCaseId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(runDetail?.cases ?? []).map((item) => (
|
||||
<option key={item.case_id} value={item.case_id}>
|
||||
{item.case_id} | {item.status}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="autoruns-case-list">
|
||||
{(runDetail?.cases ?? []).map((item) => (
|
||||
<button
|
||||
key={item.case_id}
|
||||
type="button"
|
||||
className={selectedCaseId === item.case_id ? "autoruns-case-item selected" : "autoruns-case-item"}
|
||||
onClick={() => {
|
||||
setSelectedCaseId(item.case_id);
|
||||
if (selectedRunId) {
|
||||
void loadCaseDialog(selectedRunId, item.case_id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{item.case_id}</span>
|
||||
<span>{item.status}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="autoruns-dialog-view">
|
||||
{dialogBusy || detailBusy ? <p className="muted">Загружаю диалог...</p> : null}
|
||||
{!dialogBusy && !detailBusy && (dialog?.messages.length ?? 0) === 0 ? <p className="muted">Диалог для этого кейса не найден.</p> : null}
|
||||
{(dialog?.messages ?? []).map((item, index) => {
|
||||
const role = item.role === "assistant" ? "assistant" : "user";
|
||||
return (
|
||||
<article key={`${role}-${index}`} className={`autoruns-msg ${role}`}>
|
||||
<header>
|
||||
<strong>{role === "assistant" ? "Система" : "Модель/вопрос"}</strong>
|
||||
<span>{item.created_at ? formatDateTime(item.created_at) : "n/a"}</span>
|
||||
</header>
|
||||
<p>{item.text}</p>
|
||||
{(item.trace_id || item.reply_type) && (
|
||||
<footer>
|
||||
{item.trace_id ? <span>trace={item.trace_id}</span> : null}
|
||||
{item.reply_type ? <span>reply_type={item.reply_type}</span> : null}
|
||||
</footer>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showAssistantMode ? (
|
||||
<section className="autoruns-col">
|
||||
<h3>Режим ассистента</h3>
|
||||
<div className="autoruns-meta-list">
|
||||
<div>
|
||||
<span>source:</span>
|
||||
<strong>{dialog?.source ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>session:</span>
|
||||
<strong>{dialog?.session_id ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>run target:</span>
|
||||
<strong>{activeRunSummary?.eval_target ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>run score:</span>
|
||||
<strong>{formatScore(activeRunSummary?.score_index ?? null)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<h4>Assistant mode payload</h4>
|
||||
<JsonView value={dialog?.assistant_mode ?? { note: "assistant_mode unavailable" }} />
|
||||
<h4 style={{ marginTop: 12 }}>Case checks</h4>
|
||||
<JsonView value={activeCase?.checks ?? { note: "checks unavailable" }} />
|
||||
<h4 style={{ marginTop: 12 }}>Metric subscores</h4>
|
||||
<JsonView value={activeCase?.metric_subscores ?? { note: "metric_subscores unavailable" }} />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{showDecompositionMode ? (
|
||||
<section className="autoruns-col">
|
||||
<h3>Режим декомпозиции</h3>
|
||||
<div className="autoruns-meta-list">
|
||||
<div>
|
||||
<span>Case:</span>
|
||||
<strong>{activeCase?.case_id ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Domain:</span>
|
||||
<strong>{activeCase?.domain ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Query class:</span>
|
||||
<strong>{activeCase?.query_class ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Trace:</span>
|
||||
<strong>{activeCase?.trace_id ?? "n/a"}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<h4>Шаги декомпозиции</h4>
|
||||
{(dialog?.decomposition.length ?? 0) > 0 ? (
|
||||
<ol className="autoruns-decomposition-list">
|
||||
{(dialog?.decomposition ?? []).map((item, index) => (
|
||||
<li key={`${index}-${item.slice(0, 24)}`}>{item}</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<p className="muted">В логах кейса нет явной декомпозиции.</p>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{showProgressMode ? (
|
||||
<section className="autoruns-col">
|
||||
<h3>Прогресс / регресс</h3>
|
||||
<div className="autoruns-stats-grid">
|
||||
<div>
|
||||
<span>Latest score</span>
|
||||
<strong>{formatScore(history?.stats.latest_score_index ?? null)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Previous</span>
|
||||
<strong>{formatScore(history?.stats.previous_score_index ?? null)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Trend</span>
|
||||
<strong>{history ? trendLabel(history.stats.trend) : "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Quality gaps</span>
|
||||
<strong>{history?.stats.quality_gap_runs ?? 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<h4>Покрытие доменов (история)</h4>
|
||||
{renderCoverageRows(history?.stats.domain_coverage ?? [])}
|
||||
<h4 style={{ marginTop: 14 }}>Покрытие доменов (выбранный прогон)</h4>
|
||||
{renderCoverageRows(runDetail?.coverage.domain_coverage ?? [])}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user