АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия

This commit is contained in:
2026-04-09 12:34:10 +03:00
parent df29798fa2
commit edfa09c9af
31 changed files with 5261 additions and 2392 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NDC AI Normalizer Playground</title>
<script type="module" crossorigin src="/assets/index-BFy6DcyX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ch7jCAii.css">
<script type="module" crossorigin src="/assets/index-D6Y_lHrc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BMWPMdQA.css">
</head>
<body>
<div id="root"></div>
+22 -7
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react";
import { apiClient } from "./api/client";
import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel";
import { AssistantPanel } from "./components/AssistantPanel";
import { ConnectionPanel } from "./components/ConnectionPanel";
import { HistoryPanel } from "./components/HistoryPanel";
@@ -22,7 +23,7 @@ import type {
} from "./state/types";
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
const ASSISTANT_STAGES = ["Разбираю запрос", "Ищу данные", "Собираю ответ"];
const ASSISTANT_STAGES = ["Analyzing request", "Fetching data", "Composing answer"];
const DEFAULT_UI_MODE: UiMode = "assistant";
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
@@ -508,12 +509,12 @@ export default function App() {
});
setAssistantSessionId(response.session_id);
setAssistantConversation(response.conversation);
setAssistantStatus("Ответ готов");
setAssistantStatus("Reply is ready");
log(`Assistant reply received: trace=${response.debug.trace_id}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssistantError(message);
setAssistantStatus("Ошибка ассистента");
setAssistantStatus("Assistant error");
log(`Assistant error: ${message}`);
} finally {
stopTicker();
@@ -536,15 +537,18 @@ export default function App() {
<main className="app-root">
<div className="hero">
<h1>NDC AI First Layer</h1>
<p>Два режима в одном интерфейсе: диагностика декомпозиции и диалоговый ассистент на общем backend-контуре.</p>
<p>Three modes in one UI: assistant, decomposition diagnostics, and auto-run history with regression visibility.</p>
</div>
<div className="mode-switch-row">
<button type="button" className={uiMode === "assistant" ? "tab active" : "tab"} onClick={() => setUiMode("assistant")}>
Ассистент
Assistant
</button>
<button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
Декомпозиция
Decomposition
</button>
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
AutoRun History
</button>
</div>
@@ -595,7 +599,7 @@ export default function App() {
errorMessage={assistantError}
/>
</div>
) : (
) : uiMode === "decomposition" ? (
<div className="layout-grid">
<ConnectionPanel
value={connection}
@@ -655,7 +659,18 @@ export default function App() {
evalReport={evalReport}
/>
</div>
) : (
<div className="layout-grid">
<AutoRunsHistoryPanel
connection={connection}
prompts={prompts}
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
onLog={log}
/>
</div>
)}
</main>
);
}
+34
View File
@@ -1,4 +1,7 @@
import type {
AutoRunDetailResponse,
AutoRunDialogResponse,
AutoRunHistoryResponse,
AssistantMessageResultState,
AssistantConversationItem,
ConnectionState,
@@ -247,5 +250,36 @@ export const apiClient = {
async loadAssistantSession(sessionId: string): Promise<{ ok: boolean; session: { items: AssistantConversationItem[] } }> {
return request(`/assistant/session/${sessionId}`);
},
async loadAutoRunsHistory(input?: {
from?: string;
to?: string;
target?: string;
mode?: string;
use_mock?: "any" | "true" | "false";
prompt_contains?: string;
limit?: number;
scan_limit?: number;
}): Promise<AutoRunHistoryResponse> {
const params = new URLSearchParams();
if (input?.from) params.set("from", input.from);
if (input?.to) params.set("to", input.to);
if (input?.target) params.set("target", input.target);
if (input?.mode) params.set("mode", input.mode);
if (input?.use_mock) params.set("use_mock", input.use_mock);
if (input?.prompt_contains) params.set("prompt_contains", input.prompt_contains);
if (typeof input?.limit === "number") params.set("limit", String(input.limit));
if (typeof input?.scan_limit === "number") params.set("scan_limit", String(input.scan_limit));
const query = params.toString();
return request(`/autoruns/history${query ? `?${query}` : ""}`);
},
async loadAutoRunDetail(runId: string): Promise<AutoRunDetailResponse> {
return request(`/autoruns/history/${encodeURIComponent(runId)}`);
},
async loadAutoRunCaseDialog(runId: string, caseId: string): Promise<AutoRunDialogResponse> {
return request(`/autoruns/history/${encodeURIComponent(runId)}/case/${encodeURIComponent(caseId)}/dialog`);
}
};
@@ -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>
);
}
+113 -1
View File
@@ -66,7 +66,119 @@ export interface RuntimeRun {
updatedAt: string;
}
export type UiMode = "assistant" | "decomposition";
export type UiMode = "assistant" | "decomposition" | "autoruns";
export type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
export type AutoRunTrend = "up" | "down" | "flat";
export interface AutoRunDomainCoverage {
domain: string;
total_cases: number;
closed_cases: number;
}
export interface AutoRunSummary {
run_id: string;
eval_target: AutoRunTarget;
run_timestamp: string;
mode: string | null;
llm_provider: string | null;
model: string | null;
use_mock: boolean | null;
prompt_version: string | null;
schema_version: string | null;
suite_id: string | null;
cases_total: number;
requests_total: number | null;
report_path: string;
score_index: number | null;
blocking_failures: number;
quality_failures: number;
closed_cases: number;
open_cases: number;
domain_coverage: AutoRunDomainCoverage[];
}
export interface AutoRunCaseSummary {
case_id: string;
domain: string | null;
query_class: string | null;
status: "closed" | "open" | "unknown";
score_index: number | null;
trace_id: string | null;
reply_type: string | null;
session_id: string;
dialog_available: boolean;
checks: Record<string, unknown> | null;
metric_subscores: Record<string, unknown> | null;
}
export interface AutoRunCoverage {
closed_cases: number;
open_cases: number;
domain_coverage: AutoRunDomainCoverage[];
}
export interface AutoRunHistoryStats {
runs_total: number;
by_target: Record<string, number>;
blocking_runs: number;
quality_gap_runs: number;
avg_score_index: number | null;
latest_score_index: number | null;
previous_score_index: number | null;
trend: AutoRunTrend;
domain_coverage: AutoRunDomainCoverage[];
}
export interface AutoRunHistoryResponse {
ok: boolean;
generated_at: string;
filters_applied: {
from: string | null;
to: string | null;
target: AutoRunTarget | "all";
use_mock: boolean | null;
prompt_contains: string;
mode: string;
limit: number;
scan_limit: number;
};
available: {
targets: AutoRunTarget[];
modes: string[];
prompt_versions: string[];
};
items: AutoRunSummary[];
stats: AutoRunHistoryStats;
}
export interface AutoRunDetailResponse {
ok: boolean;
run: AutoRunSummary;
coverage: AutoRunCoverage;
cases: AutoRunCaseSummary[];
report: Record<string, unknown>;
}
export interface AutoRunDialogMessage {
role: string;
text: string;
created_at: string | null;
trace_id: string | null;
reply_type: string | null;
}
export interface AutoRunDialogResponse {
ok: boolean;
run_id: string;
case_id: string;
source: "assistant_session" | "report_fallback" | "none";
session_id: string;
messages: AutoRunDialogMessage[];
decomposition: string[];
assistant_mode: Record<string, unknown> | null;
}
export type AssistantFallbackType = "none" | "out_of_scope" | "clarification" | "partial" | "unknown";
export type AssistantReplyType =
+259
View File
@@ -454,10 +454,263 @@ button:disabled {
font-size: 0.84rem;
}
.autoruns-columns {
display: grid;
gap: 12px;
}
.autoruns-col {
border: 1px solid rgba(157, 255, 190, 0.2);
border-radius: 14px;
background: rgba(8, 13, 10, 0.72);
padding: 12px;
min-height: 220px;
}
.autoruns-col h3 {
margin: 0 0 10px;
font-size: 0.95rem;
}
.autoruns-col h4 {
margin: 12px 0 8px;
font-size: 0.82rem;
color: var(--text-muted);
}
.autoruns-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.autoruns-meta-list {
display: grid;
gap: 8px;
}
.autoruns-meta-list > div {
display: flex;
justify-content: space-between;
gap: 8px;
border: 1px solid rgba(157, 255, 190, 0.14);
border-radius: 10px;
background: rgba(10, 18, 13, 0.65);
padding: 8px 9px;
font-size: 0.79rem;
}
.autoruns-meta-list span {
color: var(--text-muted);
}
.autoruns-prompt-details summary {
cursor: pointer;
color: var(--lime-main);
font-size: 0.8rem;
margin-bottom: 8px;
}
.autoruns-prompt-details textarea {
min-height: 68px;
}
.autoruns-stats-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-bottom: 10px;
}
.autoruns-stats-grid > div {
border: 1px solid rgba(157, 255, 190, 0.2);
border-radius: 10px;
background: rgba(10, 18, 13, 0.7);
padding: 8px;
display: grid;
gap: 3px;
}
.autoruns-stats-grid span {
color: var(--text-muted);
font-size: 0.74rem;
}
.autoruns-stats-grid strong {
color: var(--lime-main);
font-size: 0.84rem;
}
.autoruns-run-list {
display: grid;
gap: 8px;
max-height: 760px;
overflow: auto;
padding-right: 2px;
}
.autoruns-run-item {
width: 100%;
text-align: left;
border-radius: 12px;
border: 1px solid rgba(157, 255, 190, 0.23);
background: rgba(11, 18, 14, 0.75);
color: var(--text-main);
padding: 10px;
display: grid;
gap: 5px;
}
.autoruns-run-item.selected {
border-color: var(--line-strong);
}
.autoruns-run-head,
.autoruns-run-foot {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 0.76rem;
}
.autoruns-run-meta {
color: var(--text-muted);
font-size: 0.75rem;
word-break: break-word;
}
.autoruns-dialog-toolbar {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.autoruns-case-list {
margin-top: 8px;
display: grid;
gap: 6px;
max-height: 170px;
overflow: auto;
}
.autoruns-case-item {
width: 100%;
text-align: left;
border-radius: 10px;
border: 1px solid rgba(157, 255, 190, 0.22);
background: rgba(9, 14, 11, 0.72);
color: var(--text-main);
padding: 7px 8px;
display: flex;
justify-content: space-between;
gap: 6px;
font-size: 0.76rem;
}
.autoruns-case-item.selected {
border-color: var(--line-strong);
}
.autoruns-dialog-view {
margin-top: 10px;
border: 1px solid rgba(157, 255, 190, 0.2);
border-radius: 12px;
background: rgba(5, 8, 6, 0.7);
padding: 10px;
max-height: 570px;
overflow: auto;
display: grid;
gap: 8px;
}
.autoruns-msg {
border: 1px solid rgba(157, 255, 190, 0.22);
border-radius: 12px;
background: rgba(11, 18, 14, 0.8);
padding: 8px 10px;
display: grid;
gap: 6px;
}
.autoruns-msg header,
.autoruns-msg footer {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 0.74rem;
color: var(--text-muted);
}
.autoruns-msg p {
margin: 0;
white-space: pre-wrap;
line-height: 1.35;
font-size: 0.84rem;
}
.autoruns-msg.assistant {
margin-right: 12%;
}
.autoruns-msg.user {
margin-left: 12%;
border-color: rgba(95, 179, 255, 0.35);
background: rgba(10, 18, 24, 0.75);
}
.autoruns-decomposition-list {
margin: 0;
padding-left: 18px;
display: grid;
gap: 7px;
font-size: 0.8rem;
}
.autoruns-coverage-list {
display: grid;
gap: 8px;
}
.autoruns-coverage-item {
border: 1px solid rgba(157, 255, 190, 0.2);
border-radius: 10px;
background: rgba(11, 18, 14, 0.68);
padding: 8px;
}
.autoruns-coverage-head {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 0.76rem;
margin-bottom: 5px;
}
.autoruns-coverage-head span {
color: var(--text-muted);
}
.autoruns-coverage-bar {
height: 7px;
border-radius: 999px;
background: rgba(157, 255, 190, 0.14);
overflow: hidden;
}
.autoruns-coverage-bar > div {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, #6ee0ff, #8fffad);
}
@media (max-width: 1200px) {
.metrics-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.autoruns-columns {
grid-template-columns: 1fr !important;
}
}
@media (max-width: 920px) {
@@ -469,6 +722,12 @@ button:disabled {
.metrics-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.autoruns-form-grid,
.autoruns-dialog-toolbar,
.autoruns-stats-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/components/assistantpanel.tsx","./src/components/connectionpanel.tsx","./src/components/historypanel.tsx","./src/components/jsonview.tsx","./src/components/metricspanel.tsx","./src/components/outputpanel.tsx","./src/components/panelframe.tsx","./src/components/promptpanel.tsx","./src/components/querypanel.tsx","./src/components/runtimepanel.tsx","./src/state/defaults.ts","./src/state/types.ts","./src/utils/conversationexport.ts"],"version":"5.9.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/components/assistantpanel.tsx","./src/components/autorunshistorypanel.tsx","./src/components/connectionpanel.tsx","./src/components/historypanel.tsx","./src/components/jsonview.tsx","./src/components/metricspanel.tsx","./src/components/outputpanel.tsx","./src/components/panelframe.tsx","./src/components/promptpanel.tsx","./src/components/querypanel.tsx","./src/components/runtimepanel.tsx","./src/state/defaults.ts","./src/state/types.ts","./src/utils/conversationexport.ts"],"version":"5.9.3"}