NODEDC_1C/llm_normalizer/frontend/src/App.tsx

744 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef, useState } from "react";
import { apiClient } from "./api/client";
import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel";
import { ConnectionPanel } from "./components/ConnectionPanel";
import { HistoryPanel } from "./components/HistoryPanel";
import { MetricsPanel } from "./components/MetricsPanel";
import { OutputPanel } from "./components/OutputPanel";
import { PromptPanel } from "./components/PromptPanel";
import { QueryPanel } from "./components/QueryPanel";
import { RuntimePanel } from "./components/RuntimePanel";
import { DEFAULT_CONNECTION, DEFAULT_PROMPTS, DEFAULT_QUERY } from "./state/defaults";
import { designConfig } from "../../../designconfig";
import type {
ConnectionState,
HistoryItem,
NormalizeResultState,
PromptState,
QueryState,
RuntimeRun,
TabKey,
UiMode
} from "./state/types";
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
const AUTORUNS_LAYOUT_CONFIG_KEY = "ndc_autoruns_layout_config_v1";
const AUTORUNS_SAVE_EVENT = "ndc-autoruns-save";
const DEFAULT_UI_MODE: UiMode = "autoruns";
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
const TAB_KEYS: TabKey[] = ["normalized", "fragments", "scope", "flags", "route", "raw", "validation", "logs"];
function withTs(message: string): string {
return `[${new Date().toLocaleTimeString("ru-RU")}] ${message}`;
}
function diffPrompts(current: PromptState, previous: PromptState | null): string {
if (!previous) return "Previous preset is not selected.";
const fields: Array<keyof PromptState> = ["systemPrompt", "developerPrompt", "domainPrompt", "schemaNotes", "fewShotExamples"];
const changed = fields
.filter((field) => current[field] !== previous[field])
.map((field) => `${field}: ${Math.abs(current[field].length - previous[field].length)} chars delta`);
if (changed.length === 0) return "No changes against previous preset.";
return `Changed fields: ${changed.length}. ${changed.join(" | ")}`;
}
export default function App() {
const [connection, setConnection] = useState<ConnectionState>(DEFAULT_CONNECTION);
const [prompts, setPrompts] = useState<PromptState>(DEFAULT_PROMPTS);
const [query, setQuery] = useState<QueryState>(DEFAULT_QUERY);
const [result, setResult] = useState<NormalizeResultState | null>(null);
const [historyItems, setHistoryItems] = useState<HistoryItem[]>([]);
const [appLogs, setAppLogs] = useState<string[]>([]);
const [activeTab, setActiveTab] = useState<TabKey>("normalized");
const [busy, setBusy] = useState(false);
const [modelsBusy, setModelsBusy] = useState(false);
const [modelOptions, setModelOptions] = useState<string[]>([]);
const [connectionStatus, setConnectionStatus] = useState("");
const [presetList, setPresetList] = useState<
Array<{
id: string;
name: string;
prompt_version: string;
systemPrompt: string;
developerPrompt: string;
domainPrompt: string;
schemaNotes?: string;
fewShotExamples?: string;
}>
>([]);
const [selectedPresetId, setSelectedPresetId] = useState("");
const [presetName, setPresetName] = useState("NDC custom preset");
const [previousPreset, setPreviousPreset] = useState<PromptState | null>(null);
const [diffSummary, setDiffSummary] = useState("");
const [useMock, setUseMock] = useState(false);
const [runs, setRuns] = useState<RuntimeRun[]>([]);
const [selectedRunId, setSelectedRunId] = useState("");
const [runTrace, setRunTrace] = useState<unknown[]>([]);
const [evalBusy, setEvalBusy] = useState(false);
const [evalReport, setEvalReport] = useState<unknown>(null);
const [lastError, setLastError] = useState("");
const [uiMode, setUiMode] = useState<UiMode>(DEFAULT_UI_MODE);
const [showAutorunsSettingsMode, setShowAutorunsSettingsMode] = useState(true);
const [showAutorunsAutoRunsMode, setShowAutorunsAutoRunsMode] = useState(true);
const [showAutorunsAssistantMode, setShowAutorunsAssistantMode] = useState(true);
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = useState(true);
const [showAutorunsCommentsMode, setShowAutorunsCommentsMode] = useState(true);
const [showDecompositionConnectionMode, setShowDecompositionConnectionMode] = useState(true);
const [showDecompositionPromptMode, setShowDecompositionPromptMode] = useState(true);
const [showDecompositionQueryMode, setShowDecompositionQueryMode] = useState(true);
const [showDecompositionOutputMode, setShowDecompositionOutputMode] = useState(true);
const [showDecompositionMetricsMode, setShowDecompositionMetricsMode] = useState(true);
const [showDecompositionHistoryMode, setShowDecompositionHistoryMode] = useState(true);
const [showDecompositionRuntimeMode, setShowDecompositionRuntimeMode] = useState(true);
const presetAutoloadDoneRef = useRef(false);
const skipPresetAutoloadRef = useRef(false);
const sharedConnectionSyncReadyRef = useRef(false);
useEffect(() => {
const root = document.documentElement;
const { colors } = designConfig;
root.style.setProperty("--rgb-background", colors.backgroundRgb);
root.style.setProperty("--rgb-surface-main", colors.mainSurfaceRgb);
root.style.setProperty("--rgb-surface-horizontal", colors.horizontalSurfaceRgb);
root.style.setProperty("--rgb-surface-focus", colors.focusSurfaceRgb);
root.style.setProperty("--rgb-assistant-chip", colors.assistantChipRgb);
root.style.setProperty("--rgb-assistant-chip-hover", colors.assistantChipHoverRgb);
root.style.setProperty("--rgb-assistant-chip-selected", colors.assistantChipSelectedRgb);
root.style.setProperty("--rgb-assistant-chip-selected-text", colors.assistantChipSelectedTextRgb);
root.style.setProperty("--rgb-active", colors.activeRgb);
root.style.setProperty("--rgb-active-text", colors.activeTextRgb);
root.style.setProperty("--rgb-text-main", colors.textMainRgb);
root.style.setProperty("--rgb-text-muted", colors.textMutedRgb);
root.style.setProperty("--rgb-danger", colors.dangerRgb);
root.style.setProperty("--rgb-scrollbar-track", colors.scrollbarTrackRgb);
root.style.setProperty("--rgb-scrollbar-thumb", colors.scrollbarThumbRgb);
root.style.setProperty("--rgb-scrollbar-thumb-hover", colors.scrollbarThumbHoverRgb);
root.style.setProperty("--mode-column-width", `${designConfig.layout.modeColumnWidthPx}px`);
root.style.setProperty("--mode-toggle-width", `${designConfig.layout.modeToggleWidthPx}px`);
}, []);
const log = (message: string) => {
setAppLogs((prev) => [withTs(message), ...prev].slice(0, 300));
};
useEffect(() => {
const bootstrapSharedConnection = async () => {
const cached = localStorage.getItem(SESSION_CONFIG_KEY);
if (cached) {
try {
const parsed = JSON.parse(cached) as Partial<ConnectionState>;
setConnection((prev) => ({
...prev,
llmProvider: parsed.llmProvider === "local" ? "local" : "openai",
model: parsed.model ?? prev.model,
baseUrl: parsed.baseUrl ?? prev.baseUrl,
temperature: parsed.temperature ?? prev.temperature,
maxOutputTokens: parsed.maxOutputTokens ?? prev.maxOutputTokens
}));
} catch {
// ignore broken local cache
}
}
try {
const payload = await apiClient.loadSharedConnectionConfig();
if (payload.connection && payload.connection.llmProvider === "local") {
setConnection((prev) => ({
...prev,
llmProvider: "local",
model: payload.connection?.model ?? prev.model,
baseUrl: payload.connection?.baseUrl ?? prev.baseUrl,
temperature: payload.connection?.temperature ?? prev.temperature,
maxOutputTokens: payload.connection?.maxOutputTokens ?? prev.maxOutputTokens
}));
log(`Shared local LLM config loaded: ${payload.connection.model}`);
}
} catch (error) {
log(`Shared local config load error: ${error instanceof Error ? error.message : String(error)}`);
} finally {
sharedConnectionSyncReadyRef.current = true;
}
};
void bootstrapSharedConnection();
const cachedAutorunsLayout = localStorage.getItem(AUTORUNS_LAYOUT_CONFIG_KEY);
if (cachedAutorunsLayout) {
try {
const parsed = JSON.parse(cachedAutorunsLayout) as {
uiMode?: UiMode | "assistant";
activeTab?: TabKey;
showAutorunsSettingsMode?: boolean;
showAutorunsAutoRunsMode?: boolean;
showAutorunsAssistantMode?: boolean;
showAutorunsDecompositionMode?: boolean;
showAutorunsProgressMode?: boolean;
showAutorunsCommentsMode?: boolean;
showDecompositionConnectionMode?: boolean;
showDecompositionPromptMode?: boolean;
showDecompositionQueryMode?: boolean;
showDecompositionOutputMode?: boolean;
showDecompositionMetricsMode?: boolean;
showDecompositionHistoryMode?: boolean;
showDecompositionRuntimeMode?: boolean;
prompts?: PromptState;
};
if (parsed.uiMode === "assistant" || parsed.uiMode === "autoruns" || parsed.uiMode === "decomposition") {
setUiMode("autoruns");
}
if (parsed.activeTab && TAB_KEYS.includes(parsed.activeTab)) {
setActiveTab(parsed.activeTab);
}
if (typeof parsed.showAutorunsSettingsMode === "boolean") {
setShowAutorunsSettingsMode(parsed.showAutorunsSettingsMode);
}
if (typeof parsed.showAutorunsAutoRunsMode === "boolean") {
setShowAutorunsAutoRunsMode(parsed.showAutorunsAutoRunsMode);
}
if (typeof parsed.showAutorunsAssistantMode === "boolean") {
setShowAutorunsAssistantMode(parsed.showAutorunsAssistantMode);
}
if (typeof parsed.showAutorunsDecompositionMode === "boolean") {
setShowAutorunsDecompositionMode(parsed.showAutorunsDecompositionMode);
}
if (typeof parsed.showAutorunsProgressMode === "boolean") {
setShowAutorunsProgressMode(parsed.showAutorunsProgressMode);
}
if (typeof parsed.showAutorunsCommentsMode === "boolean") {
setShowAutorunsCommentsMode(parsed.showAutorunsCommentsMode);
}
if (typeof parsed.showDecompositionConnectionMode === "boolean") {
setShowDecompositionConnectionMode(parsed.showDecompositionConnectionMode);
}
if (typeof parsed.showDecompositionPromptMode === "boolean") {
setShowDecompositionPromptMode(parsed.showDecompositionPromptMode);
}
if (typeof parsed.showDecompositionQueryMode === "boolean") {
setShowDecompositionQueryMode(parsed.showDecompositionQueryMode);
}
if (typeof parsed.showDecompositionOutputMode === "boolean") {
setShowDecompositionOutputMode(parsed.showDecompositionOutputMode);
}
if (typeof parsed.showDecompositionMetricsMode === "boolean") {
setShowDecompositionMetricsMode(parsed.showDecompositionMetricsMode);
}
if (typeof parsed.showDecompositionHistoryMode === "boolean") {
setShowDecompositionHistoryMode(parsed.showDecompositionHistoryMode);
}
if (typeof parsed.showDecompositionRuntimeMode === "boolean") {
setShowDecompositionRuntimeMode(parsed.showDecompositionRuntimeMode);
}
if (parsed.prompts) {
setPrompts((prev) => ({
...prev,
...parsed.prompts
}));
skipPresetAutoloadRef.current = true;
}
} catch {
// ignore broken local cache
}
}
void refreshHistory();
void refreshPresets();
void refreshRuns();
}, []);
useEffect(() => {
if (!sharedConnectionSyncReadyRef.current) {
return;
}
if (connection.llmProvider !== "local") {
return;
}
const timer = window.setTimeout(() => {
void apiClient
.saveSharedConnectionConfig(connection)
.catch((error) =>
log(`Shared local config sync error: ${error instanceof Error ? error.message : String(error)}`)
);
}, 250);
return () => window.clearTimeout(timer);
}, [connection.baseUrl, connection.llmProvider, connection.maxOutputTokens, connection.model, connection.temperature]);
async function refreshHistory() {
try {
const payload = await apiClient.loadHistory();
setHistoryItems(payload.items ?? []);
} catch (error) {
log(`History load error: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function refreshPresets() {
try {
const payload = await apiClient.loadPresets();
const presets = payload.presets ?? [];
setPresetList(presets);
if (skipPresetAutoloadRef.current) {
presetAutoloadDoneRef.current = true;
return;
}
if (presetAutoloadDoneRef.current) {
return;
}
const presetForAutoload =
presets.find((item) => item.prompt_version === AUTOLOAD_PROMPT_VERSION) ??
presets.find((item) => item.id === "default-normalizer-v2_0_2");
if (!presetForAutoload) {
presetAutoloadDoneRef.current = true;
log(`Preset autoload skipped: ${AUTOLOAD_PROMPT_VERSION} not found.`);
return;
}
setSelectedPresetId(presetForAutoload.id);
setPreviousPreset(prompts);
setPrompts({
systemPrompt: presetForAutoload.systemPrompt,
developerPrompt: presetForAutoload.developerPrompt,
domainPrompt: presetForAutoload.domainPrompt,
schemaNotes: presetForAutoload.schemaNotes ?? "",
fewShotExamples: presetForAutoload.fewShotExamples ?? ""
});
presetAutoloadDoneRef.current = true;
log(`Preset autoloaded: ${presetForAutoload.name} (${presetForAutoload.prompt_version}).`);
} catch (error) {
log(`Presets load error: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function refreshRuns() {
try {
const payload = await apiClient.listRuns();
setRuns(payload.items ?? []);
} catch (error) {
log(`Runs load error: ${error instanceof Error ? error.message : String(error)}`);
}
}
function saveLocalConfig() {
localStorage.setItem(
SESSION_CONFIG_KEY,
JSON.stringify({
model: connection.model,
llmProvider: connection.llmProvider,
baseUrl: connection.baseUrl,
temperature: connection.temperature,
maxOutputTokens: connection.maxOutputTokens
})
);
if (connection.llmProvider === "local") {
void apiClient
.saveSharedConnectionConfig(connection)
.then(() => {
log("Local config saved and synced to shared agent config (without API key).");
})
.catch((error) => {
log(`Local config saved, but shared sync failed: ${error instanceof Error ? error.message : String(error)}`);
});
return;
}
log("Local config saved (without API key).");
}
function saveAutorunsLayout() {
localStorage.setItem(
AUTORUNS_LAYOUT_CONFIG_KEY,
JSON.stringify({
uiMode,
activeTab,
showAutorunsSettingsMode,
showAutorunsAutoRunsMode,
showAutorunsAssistantMode,
showAutorunsDecompositionMode,
showAutorunsProgressMode,
showAutorunsCommentsMode,
showDecompositionConnectionMode,
showDecompositionPromptMode,
showDecompositionQueryMode,
showDecompositionOutputMode,
showDecompositionMetricsMode,
showDecompositionHistoryMode,
showDecompositionRuntimeMode,
prompts
})
);
window.dispatchEvent(new CustomEvent(AUTORUNS_SAVE_EVENT));
log("UI layout and prompts saved.");
}
async function testConnection() {
setBusy(true);
setLastError("");
try {
const payload = await apiClient.testConnection(connection);
if (payload.provider === "local") {
if (payload.model_found === true) {
setConnectionStatus(`LOCAL OK - ${payload.model}`);
log(`Local model is available: ${payload.model} (catalog size=${payload.models_count ?? "n/a"}).`);
} else if (payload.model_found === false) {
setConnectionStatus(`LOCAL OK, model not loaded - ${payload.model}`);
log(
`Local server is reachable, but model '${payload.model}' is not in loaded catalog. ` +
`Use 'Load model list' and select one of loaded models.`
);
} else {
setConnectionStatus(`LOCAL OK (model list unavailable) - ${payload.model}`);
log("Local server is reachable, but model catalog could not be verified.");
}
} else {
setConnectionStatus(`OPENAI OK - ${payload.model}`);
log(`OpenAI connection ok: ${payload.model}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setConnectionStatus("Connection error");
setLastError(`Test connection: ${message}`);
log(`Test connection error: ${message}`);
} finally {
setBusy(false);
}
}
async function reloadModels() {
setModelsBusy(true);
try {
const payload = await apiClient.listModels(connection);
const models = payload.models ?? [];
setModelOptions(models);
if (models.length > 0) {
setConnection((prev) => {
if (prev.model && models.includes(prev.model)) {
return prev;
}
return { ...prev, model: models[0] };
});
}
log(`Model catalog loaded (${connection.llmProvider}): ${models.length} items.`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log(`Load model list error: ${message}`);
} finally {
setModelsBusy(false);
}
}
useEffect(() => {
setModelOptions([]);
}, [connection.llmProvider, connection.baseUrl]);
async function normalize(saveAsCase: boolean) {
setBusy(true);
setLastError("");
try {
const payload = await apiClient.normalize({
connection,
prompts,
promptVersion: "normalizer_v2_0_2",
query: {
userQuestion: query.userQuestion,
periodHint: query.periodHint,
businessContext: query.businessContext,
expectedRoute: query.expectedRoute
},
saveAsTestCase: saveAsCase,
useMock
});
setResult(payload);
setActiveTab("normalized");
log(`Normalize done: trace=${payload.trace_id}, validation=${payload.validation.passed ? "passed" : "failed"}`);
void refreshHistory();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setLastError(`Normalize: ${message}`);
log(`Normalize error: ${message}`);
} finally {
setBusy(false);
}
}
function loadSelectedPreset() {
const selected = presetList.find((item) => item.id === selectedPresetId);
if (!selected) {
log("Preset is not selected.");
return;
}
setPreviousPreset(prompts);
setPrompts({
systemPrompt: selected.systemPrompt,
developerPrompt: selected.developerPrompt,
domainPrompt: selected.domainPrompt,
schemaNotes: selected.schemaNotes ?? "",
fewShotExamples: selected.fewShotExamples ?? ""
});
log(`Preset loaded: ${selected.name}`);
}
async function savePreset() {
try {
await apiClient.savePreset({
name: presetName || "NDC preset",
prompt_version: "normalizer_v2_0_2",
systemPrompt: prompts.systemPrompt,
developerPrompt: prompts.developerPrompt,
domainPrompt: prompts.domainPrompt,
schemaNotes: prompts.schemaNotes,
fewShotExamples: prompts.fewShotExamples
});
log("Preset saved.");
await refreshPresets();
} catch (error) {
log(`Preset save error: ${error instanceof Error ? error.message : String(error)}`);
}
}
function resetDefaults() {
setPrompts(DEFAULT_PROMPTS);
log("Prompt panel reset to defaults.");
}
function diffWithPrevious() {
const summary = diffPrompts(prompts, previousPreset);
setDiffSummary(summary);
log(summary);
}
function applyBatchFormat() {
const formatted = query.batchQuestionsRaw
.split(";")
.map((item) => item.trim())
.filter(Boolean)
.join("\n\n");
if (!formatted) {
return;
}
setQuery((prev) => ({
...prev,
batchQuestionsRaw: formatted
}));
log("Batch field formatted: `;` converted to blank-line separators.");
}
async function openTrace(traceId: string) {
try {
const payload = await apiClient.loadTrace(traceId);
const trace = payload.trace as Record<string, unknown>;
const normalized = (trace.parsed_normalized_json ?? null) as NormalizeResultState["normalized"];
setResult({
trace_id: String(trace.trace_id ?? traceId),
ok: Boolean((trace.validation_result as { passed?: boolean } | undefined)?.passed),
normalized,
route_hint_summary:
(trace.route_hint_summary as NormalizeResultState["route_hint_summary"] | undefined) ??
(normalized
? {
route_hint: (normalized as { route_hint?: string }).route_hint ?? null,
confidence: (normalized as { confidence?: { route_hint?: string } }).confidence?.route_hint ?? null
}
: null),
raw_model_output: trace.raw_model_response ?? {},
validation: (trace.validation_result as NormalizeResultState["validation"]) ?? { passed: false, errors: ["validation not found"] },
usage: (trace.usage as NormalizeResultState["usage"]) ?? { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: Number(trace.latency_ms ?? 0),
prompt_version: String(trace.prompt_version ?? "unknown"),
schema_version: String(trace.schema_version ?? "unknown")
});
setActiveTab("raw");
setLastError("");
log(`Trace opened: ${traceId}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setLastError(`Trace: ${message}`);
log(`Trace open error ${traceId}: ${message}`);
}
}
async function startRun() {
try {
const payload = await apiClient.startRun();
setSelectedRunId(payload.run.runId);
log(`Run started: ${payload.run.runId}`);
log("Tip: start run does not execute normalize by itself. Use 'Run eval v2.0.2' button.");
await refreshRuns();
} catch (error) {
log(`Run start error: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function finishRun() {
if (!selectedRunId) return;
try {
await apiClient.finishRun(selectedRunId);
log(`Run finished: ${selectedRunId}`);
await refreshRuns();
} catch (error) {
log(`Run finish error: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function runEval() {
setEvalBusy(true);
setLastError("");
try {
log("Starting eval in v2 contour.");
const rawQuestions = query.batchQuestionsRaw.trim() || query.userQuestion.trim();
if (!rawQuestions) {
throw new Error("Fill batch field or Raw user question first.");
}
const payload = await apiClient.runEval({
connection,
prompts,
promptVersion: "normalizer_v2_0_2",
mode: "single-pass-strict",
rawQuestions,
useMock
});
setEvalReport(payload.report);
log("Eval v2.0.2 run finished.");
const report = payload.report as { metrics?: Record<string, unknown>; run_id?: string };
if (report.run_id) {
log(`Eval run id: ${report.run_id}`);
}
if (report.metrics) {
const metrics = report.metrics;
log(
`Eval metrics v2.0.2: schema=${metrics.schema_validation_pass_rate ?? "n/a"}%, route_accuracy=${metrics.route_resolution_accuracy ?? "n/a"}%, no_route_precision=${metrics.no_route_precision ?? "n/a"}%, state_consistency=${metrics.execution_state_consistency_rate ?? "n/a"}%`
);
}
await refreshHistory();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("Legacy eval runner supports normalized_query_v1 only")) {
setEvalReport({
status: "plan_only",
prompt_version: "normalizer_v2",
reason: "backend eval runner is still legacy-v1 only",
plan_file: "reports/v2_pilot_eval_plan.md",
next_steps: [
"run cheap mock sanity for schema/fragment/scope",
"run small real batch (10-15 messages, temperature=0)",
"run challenge-30 replay with v2 metrics"
]
});
log("Backend is legacy-only for eval right now. Showing v2 pilot plan.");
} else {
setLastError(`Eval: ${message}`);
log(`Eval run error: ${message}`);
}
} finally {
setEvalBusy(false);
}
}
async function copyEvalReport() {
try {
const text = JSON.stringify(evalReport ?? {}, null, 2);
await navigator.clipboard.writeText(text);
log("Eval report copied to clipboard.");
} catch (error) {
log(`Eval report copy error: ${error instanceof Error ? error.message : String(error)}`);
}
}
useEffect(() => {
if (!selectedRunId) {
setRunTrace([]);
return;
}
void apiClient
.runTrace(selectedRunId)
.then((payload) => setRunTrace(payload.items))
.catch((error) => log(`Run trace error: ${error instanceof Error ? error.message : String(error)}`));
}, [selectedRunId]);
return (
<main className="app-root app-root-autoruns">
<header className="app-topbar">
<div className="mode-switch-row">
<button type="button" className="tab active" onClick={() => setUiMode("autoruns")}>
Управление ассистентом
</button>
<button type="button" className="tab" onClick={saveAutorunsLayout}>
Сохранить
</button>
</div>
<div className="mode-switch-row mode-switch-row-right">
<button
type="button"
className={showAutorunsSettingsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsSettingsMode((prev) => !prev)}
>
Настройки
</button>
<button
type="button"
className={showAutorunsAutoRunsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsAutoRunsMode((prev) => !prev)}
>
Автопрогоны
</button>
<button
type="button"
className={showAutorunsAssistantMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsAssistantMode((prev) => !prev)}
>
Режим ассистента
</button>
<button
type="button"
className={showAutorunsProgressMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsProgressMode((prev) => !prev)}
>
Прогресс/регресс
</button>
<button
type="button"
className={showAutorunsCommentsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsCommentsMode((prev) => !prev)}
>
Комментарии
</button>
</div>
</header>
<div className="layout-grid layout-grid-autoruns">
<AutoRunsHistoryPanel
connection={connection}
modelOptions={modelOptions}
modelsBusy={modelsBusy}
connectionStatus={connectionStatus}
connectionBusy={busy}
onConnectionChange={setConnection}
onReloadModels={reloadModels}
onSaveLocalConfig={saveLocalConfig}
onTestConnection={testConnection}
prompts={prompts}
onPromptsChange={setPrompts}
promptPresets={presetList}
selectedPresetId={selectedPresetId}
onSelectPreset={setSelectedPresetId}
onLoadPreset={loadSelectedPreset}
onSavePreset={savePreset}
onResetDefaults={resetDefaults}
onDiffPrevious={diffWithPrevious}
presetName={presetName}
onPresetNameChange={setPresetName}
diffSummary={diffSummary}
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
showSettingsMode={showAutorunsSettingsMode}
showAutoRunsMode={showAutorunsAutoRunsMode}
showAssistantMode={showAutorunsAssistantMode}
showProgressMode={showAutorunsProgressMode}
showCommentsMode={showAutorunsCommentsMode}
onLog={log}
/>
</div>
</main>
);
}