744 lines
29 KiB
TypeScript
744 lines
29 KiB
TypeScript
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>
|
||
);
|
||
}
|