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 = ["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(DEFAULT_CONNECTION); const [prompts, setPrompts] = useState(DEFAULT_PROMPTS); const [query, setQuery] = useState(DEFAULT_QUERY); const [result, setResult] = useState(null); const [historyItems, setHistoryItems] = useState([]); const [appLogs, setAppLogs] = useState([]); const [activeTab, setActiveTab] = useState("normalized"); const [busy, setBusy] = useState(false); const [modelsBusy, setModelsBusy] = useState(false); const [modelOptions, setModelOptions] = useState([]); 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(null); const [diffSummary, setDiffSummary] = useState(""); const [useMock, setUseMock] = useState(false); const [runs, setRuns] = useState([]); const [selectedRunId, setSelectedRunId] = useState(""); const [runTrace, setRunTrace] = useState([]); const [evalBusy, setEvalBusy] = useState(false); const [evalReport, setEvalReport] = useState(null); const [lastError, setLastError] = useState(""); const [uiMode, setUiMode] = useState(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; 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; 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; 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 (
); }