import { useEffect, useMemo, useRef, useState } from "react"; import { apiClient } from "./api/client"; import { AssistantSamPanel } from "./components/AssistantSamPanel"; import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel"; import { AssistantPanel } from "./components/AssistantPanel"; import { ConnectionPanel } from "./components/ConnectionPanel"; import { HistoryPanel } from "./components/HistoryPanel"; import { MetricsPanel } from "./components/MetricsPanel"; import { OutputPanel } from "./components/OutputPanel"; import { PanelFrame } from "./components/PanelFrame"; 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 { AssistantConversationItem, AssistantAnnotationRecord, AssistantSelectionChip, 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 ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"]; 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"]; const DEFAULT_ASSISTANT_ANNOTATION_AUTHOR = "manual_reviewer"; interface AssistantCommentModalState { open: boolean; messageIndex: number; rating: number; comment: string; annotationAuthor: string; saving: boolean; error: string; } 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(" | ")}`; } function buildAssistantFollowupMessage(inputValue: string, selectedChip: AssistantSelectionChip | null): string { const trimmedInput = inputValue.trim(); if (!trimmedInput) { return ""; } if (!selectedChip) { return trimmedInput; } const normalizedInput = trimmedInput.toLowerCase(); const selectionAnchor = selectedChip.anchor_text.trim(); const normalizedSelection = selectionAnchor.toLowerCase(); if (normalizedSelection && normalizedInput.includes(normalizedSelection)) { return trimmedInput; } return `По выбранному объекту "${selectionAnchor}": ${trimmedInput}`; } 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 [showAssistantConnectionMode, setShowAssistantConnectionMode] = useState(true); const [showAssistantPromptMode, setShowAssistantPromptMode] = useState(true); const [showAssistantChatMode, setShowAssistantChatMode] = useState(true); const [showAssistantCommentsMode, setShowAssistantCommentsMode] = useState(true); const [showAssistantSamMode, setShowAssistantSamMode] = 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 [assistantSessionId, setAssistantSessionId] = useState(""); const [assistantConversation, setAssistantConversation] = useState([]); const [assistantInput, setAssistantInput] = useState(""); const [assistantSelectedChip, setAssistantSelectedChip] = useState(null); const [assistantBusy, setAssistantBusy] = useState(false); const [assistantStatus, setAssistantStatus] = useState(""); const [assistantError, setAssistantError] = useState(""); const [assistantAnnotations, setAssistantAnnotations] = useState([]); const [assistantAnnotationsBusy, setAssistantAnnotationsBusy] = useState(false); const [assistantCommentModal, setAssistantCommentModal] = useState({ open: false, messageIndex: -1, rating: 3, comment: "", annotationAuthor: DEFAULT_ASSISTANT_ANNOTATION_AUTHOR, saving: false, error: "" }); 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)); }; function startAssistantStatusTicker(): () => void { let index = 0; setAssistantStatus(ASSISTANT_STAGES[0]); const timer = window.setInterval(() => { index = Math.min(index + 1, ASSISTANT_STAGES.length - 1); setAssistantStatus(ASSISTANT_STAGES[index]); }, 650); return () => window.clearInterval(timer); } 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; activeTab?: TabKey; showAutorunsSettingsMode?: boolean; showAutorunsAutoRunsMode?: boolean; showAutorunsAssistantMode?: boolean; showAutorunsDecompositionMode?: boolean; showAutorunsProgressMode?: boolean; showAutorunsCommentsMode?: boolean; showAssistantConnectionMode?: boolean; showAssistantPromptMode?: boolean; showAssistantChatMode?: boolean; showAssistantCommentsMode?: boolean; showAssistantSamMode?: boolean; showDecompositionConnectionMode?: boolean; showDecompositionPromptMode?: boolean; showDecompositionQueryMode?: boolean; showDecompositionOutputMode?: boolean; showDecompositionMetricsMode?: boolean; showDecompositionHistoryMode?: boolean; showDecompositionRuntimeMode?: boolean; prompts?: PromptState; }; if (parsed.uiMode === "decomposition") { setUiMode("decomposition"); } else if (parsed.uiMode === "assistant" || parsed.uiMode === "autoruns") { 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.showAssistantConnectionMode === "boolean") { setShowAssistantConnectionMode(parsed.showAssistantConnectionMode); } if (typeof parsed.showAssistantPromptMode === "boolean") { setShowAssistantPromptMode(parsed.showAssistantPromptMode); } if (typeof parsed.showAssistantChatMode === "boolean") { setShowAssistantChatMode(parsed.showAssistantChatMode); } if (typeof parsed.showAssistantCommentsMode === "boolean") { setShowAssistantCommentsMode(parsed.showAssistantCommentsMode); } if (typeof parsed.showAssistantSamMode === "boolean") { setShowAssistantSamMode(parsed.showAssistantSamMode); } 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, showAssistantConnectionMode, showAssistantPromptMode, showAssistantChatMode, showAssistantCommentsMode, showAssistantSamMode, 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)}`); } } const assistantAnnotationsByMessageId = useMemo(() => { const map = new Map(); for (const item of assistantAnnotations) { if (item.message_id) { map.set(item.message_id, item); } } return map; }, [assistantAnnotations]); const assistantCommentModalMessage = assistantCommentModal.messageIndex >= 0 ? assistantConversation[assistantCommentModal.messageIndex] ?? null : null; const assistantCommentModalQuestion = useMemo(() => { if (assistantCommentModal.messageIndex < 0) return null; for (let index = assistantCommentModal.messageIndex - 1; index >= 0; index -= 1) { const candidate = assistantConversation[index]; if (candidate?.role === "user") { return candidate; } } return null; }, [assistantCommentModal.messageIndex, assistantConversation]); async function loadAssistantAnnotationsForSession(sessionId: string): Promise { if (!sessionId.trim()) { setAssistantAnnotations([]); return; } setAssistantAnnotationsBusy(true); try { const payload = await apiClient.loadAssistantAnnotations({ session_id: sessionId, limit: 400 }); setAssistantAnnotations(payload.items ?? []); } catch (error) { const message = error instanceof Error ? error.message : String(error); log(`Assistant annotations load error: ${message}`); } finally { setAssistantAnnotationsBusy(false); } } function closeAssistantCommentModal(options?: { force?: boolean }) { setAssistantCommentModal((prev) => { if (prev.saving && !options?.force) { return prev; } return { open: false, messageIndex: -1, rating: 3, comment: "", annotationAuthor: DEFAULT_ASSISTANT_ANNOTATION_AUTHOR, saving: false, error: "" }; }); } function openAssistantCommentModal(item: AssistantConversationItem, index: number): void { if (item.role !== "assistant") return; const sessionIdFromState = assistantSessionId.trim(); const sessionIdFromItem = String(item.session_id ?? "").trim(); const resolvedSessionId = sessionIdFromState || sessionIdFromItem; if (!resolvedSessionId) { setAssistantError("Сначала получите ответ ассистента в активной сессии."); return; } if (!sessionIdFromState && sessionIdFromItem) { setAssistantSessionId(sessionIdFromItem); } const existing = assistantAnnotationsByMessageId.get(item.message_id) ?? null; setAssistantCommentModal({ open: true, messageIndex: index, rating: existing?.rating ?? 3, comment: existing?.comment ?? "", annotationAuthor: existing?.annotation_author ?? DEFAULT_ASSISTANT_ANNOTATION_AUTHOR, saving: false, error: "" }); } function canCommentAssistantMessage(item: AssistantConversationItem): boolean { return item.role === "assistant"; } function isAssistantMessageCommented(item: AssistantConversationItem): boolean { return item.role === "assistant" && assistantAnnotationsByMessageId.has(item.message_id); } async function submitAssistantCommentModal(): Promise { if (!assistantSessionId.trim()) { setAssistantCommentModal((prev) => ({ ...prev, error: "Сессия ассистента не найдена." })); return; } if (assistantCommentModal.messageIndex < 0) { return; } if (!assistantCommentModal.comment.trim()) { setAssistantCommentModal((prev) => ({ ...prev, error: "Добавьте комментарий." })); return; } setAssistantCommentModal((prev) => ({ ...prev, saving: true, error: "" })); try { const payload = await apiClient.saveAssistantAnnotation({ session_id: assistantSessionId, message_index: assistantCommentModal.messageIndex, rating: assistantCommentModal.rating, comment: assistantCommentModal.comment.trim(), annotation_author: assistantCommentModal.annotationAuthor.trim() || undefined }); setAssistantAnnotations((prev) => { const next = [...prev]; const index = next.findIndex((item) => item.annotation_id === payload.annotation.annotation_id); if (index >= 0) { next[index] = payload.annotation; } else { next.unshift(payload.annotation); } return next.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at)); }); closeAssistantCommentModal({ force: true }); } catch (error) { const message = error instanceof Error ? error.message : String(error); setAssistantCommentModal((prev) => ({ ...prev, saving: false, error: message })); } } function resetAssistantSession() { setAssistantSessionId(""); setAssistantConversation([]); setAssistantInput(""); setAssistantSelectedChip(null); setAssistantStatus(""); setAssistantError(""); setAssistantAnnotations([]); closeAssistantCommentModal({ force: true }); log("Assistant session reset."); } async function sendAssistantMessage() { const userMessage = buildAssistantFollowupMessage(assistantInput, assistantSelectedChip); if (!userMessage) { return; } setAssistantBusy(true); setAssistantError(""); setAssistantInput(""); setAssistantConversation((prev) => [ ...prev, { message_id: `local-${Date.now()}`, session_id: assistantSessionId || "pending", role: "user", text: userMessage, reply_type: null, created_at: new Date().toISOString(), trace_id: null, debug: null } ]); const stopTicker = startAssistantStatusTicker(); try { const response = await apiClient.sendAssistantMessage({ connection, prompts, userMessage, sessionId: assistantSessionId || undefined, promptVersion: ASSISTANT_PROMPT_VERSION, useMock }); setAssistantSessionId(response.session_id); setAssistantConversation(response.conversation); setAssistantStatus("Ответ готов"); await loadAssistantAnnotationsForSession(response.session_id); log(`Assistant reply received: trace=${response.debug.trace_id}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); setAssistantError(message); setAssistantStatus("Ошибка ассистента"); log(`Assistant error: ${message}`); } finally { stopTicker(); setAssistantBusy(false); } } useEffect(() => { if (!assistantSessionId.trim()) { setAssistantAnnotations([]); return; } void loadAssistantAnnotationsForSession(assistantSessionId); }, [assistantSessionId]); 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 (
{uiMode === "assistant" ? (
) : uiMode === "decomposition" ? (
) : uiMode === "autoruns" ? (
) : null}
{uiMode === "assistant" ? (
{showAssistantConnectionMode ? (
) : null} {showAssistantPromptMode ? (
) : null} {showAssistantChatMode ? (
setAssistantSelectedChip(null)} useMock={useMock} onUseMockChange={setUseMock} onSend={sendAssistantMessage} onClear={resetAssistantSession} busy={assistantBusy} statusText={assistantStatus} errorMessage={assistantError} showCommentAction onCommentAssistantMessage={openAssistantCommentModal} isAssistantMessageCommented={isAssistantMessageCommented} canCommentAssistantMessage={canCommentAssistantMessage} />
) : null} {showAssistantCommentsMode ? (
{assistantSessionId ? `session: ${assistantSessionId}` : "Сессия не запущена"}
{!assistantSessionId ?

Появится после первого ответа ассистента.

: null} {assistantSessionId && assistantAnnotations.length === 0 && !assistantAnnotationsBusy ? (

Комментариев по этой сессии пока нет.

) : null} {assistantAnnotations.map((item) => (
{`${"●".repeat(Math.max(1, Math.min(5, Math.round(item.rating))))}${"○".repeat(Math.max(0, 5 - Math.round(item.rating)))}`} {new Date(item.updated_at).toLocaleString("ru-RU")}
{item.context.question_text ?

Q: {item.context.question_text}

: null} {item.context.answer_text ?

A: {item.context.answer_text}

: null}

{item.comment}

{item.context.trace_id ? {`trace=${item.context.trace_id}`} : null} {item.context.reply_type ? {`reply_type=${item.context.reply_type}`} : null}
))}
) : null} {showAssistantSamMode ? (
) : null} {!showAssistantConnectionMode && !showAssistantPromptMode && !showAssistantChatMode && !showAssistantCommentsMode && !showAssistantSamMode ? (
Все панели режима ассистента скрыты. Включите нужные блоки справа в шапке.
) : null}
) : uiMode === "decomposition" ? (
{showDecompositionConnectionMode ? (
) : null} {showDecompositionPromptMode ? (
) : null} {showDecompositionQueryMode ? (
) : null} {showDecompositionOutputMode ? (
) : null} {showDecompositionMetricsMode ? (
) : null} {showDecompositionHistoryMode ? (
) : null} {showDecompositionRuntimeMode ? (
) : null} {!showDecompositionConnectionMode && !showDecompositionPromptMode && !showDecompositionQueryMode && !showDecompositionOutputMode && !showDecompositionMetricsMode && !showDecompositionHistoryMode && !showDecompositionRuntimeMode ? (
Все панели режима декомпозиции скрыты. Включите нужные блоки справа в шапке.
) : null}
) : (
)} {assistantCommentModal.open ? (
{ if (event.target === event.currentTarget) { closeAssistantCommentModal(); } }} >

Комментарий к ответу ассистента

Эта разметка хранится отдельно от комментариев автопрогонов.

{assistantCommentModalQuestion ? (
Вопрос пользователя

{assistantCommentModalQuestion.text}

) : null} {assistantCommentModalMessage ? (
Ответ ассистента

{assistantCommentModalMessage.text}

) : null}
{[1, 2, 3, 4, 5].map((value) => ( ))}