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

This commit is contained in:
dctouch 2026-04-09 17:33:32 +03:00
parent 60a4bef88c
commit cf5ba1afc2
15 changed files with 904 additions and 305 deletions

View File

@ -12,6 +12,10 @@ export const designConfig = {
scrollbarTrackRgb: "20, 20, 20", scrollbarTrackRgb: "20, 20, 20",
scrollbarThumbRgb: "30, 30, 30", scrollbarThumbRgb: "30, 30, 30",
scrollbarThumbHoverRgb: "30, 50, 30" scrollbarThumbHoverRgb: "30, 50, 30"
},
layout: {
modeColumnWidthPx: 440,
modeToggleWidthPx: 188
} }
} as const; } as const;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NDC AI Normalizer Playground</title> <title>NDC AI Normalizer Playground</title>
<script type="module" crossorigin src="/assets/index-BXQlrB3i.js"></script> <script type="module" crossorigin src="/assets/index-Cbn_mHUl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BVc11Mnb.css"> <link rel="stylesheet" crossorigin href="/assets/index-BT0bMOoF.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { apiClient } from "./api/client"; import { apiClient } from "./api/client";
import { AssistantSamPanel } from "./components/AssistantSamPanel";
import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel"; import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel";
import { AssistantPanel } from "./components/AssistantPanel"; import { AssistantPanel } from "./components/AssistantPanel";
import { ConnectionPanel } from "./components/ConnectionPanel"; import { ConnectionPanel } from "./components/ConnectionPanel";
@ -24,10 +25,13 @@ import type {
} from "./state/types"; } from "./state/types";
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1"; 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 ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"];
const DEFAULT_UI_MODE: UiMode = "assistant"; const DEFAULT_UI_MODE: UiMode = "assistant";
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2"; const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1"; 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 { function withTs(message: string): string {
return `[${new Date().toLocaleTimeString("ru-RU")}] ${message}`; return `[${new Date().toLocaleTimeString("ru-RU")}] ${message}`;
@ -83,6 +87,18 @@ export default function App() {
const [showAutorunsAssistantMode, setShowAutorunsAssistantMode] = useState(true); const [showAutorunsAssistantMode, setShowAutorunsAssistantMode] = useState(true);
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true); const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = 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 [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 [assistantSessionId, setAssistantSessionId] = useState("");
const [assistantConversation, setAssistantConversation] = useState<AssistantConversationItem[]>([]); const [assistantConversation, setAssistantConversation] = useState<AssistantConversationItem[]>([]);
const [assistantInput, setAssistantInput] = useState(""); const [assistantInput, setAssistantInput] = useState("");
@ -90,6 +106,7 @@ export default function App() {
const [assistantStatus, setAssistantStatus] = useState(""); const [assistantStatus, setAssistantStatus] = useState("");
const [assistantError, setAssistantError] = useState(""); const [assistantError, setAssistantError] = useState("");
const presetAutoloadDoneRef = useRef(false); const presetAutoloadDoneRef = useRef(false);
const skipPresetAutoloadRef = useRef(false);
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
@ -106,6 +123,8 @@ export default function App() {
root.style.setProperty("--rgb-scrollbar-track", colors.scrollbarTrackRgb); root.style.setProperty("--rgb-scrollbar-track", colors.scrollbarTrackRgb);
root.style.setProperty("--rgb-scrollbar-thumb", colors.scrollbarThumbRgb); root.style.setProperty("--rgb-scrollbar-thumb", colors.scrollbarThumbRgb);
root.style.setProperty("--rgb-scrollbar-thumb-hover", colors.scrollbarThumbHoverRgb); 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) => { const log = (message: string) => {
@ -140,6 +159,92 @@ export default function App() {
} }
} }
const cachedAutorunsLayout = localStorage.getItem(AUTORUNS_LAYOUT_CONFIG_KEY);
if (cachedAutorunsLayout) {
try {
const parsed = JSON.parse(cachedAutorunsLayout) as {
uiMode?: UiMode;
activeTab?: TabKey;
showAutorunsAssistantMode?: boolean;
showAutorunsDecompositionMode?: boolean;
showAutorunsProgressMode?: boolean;
showAutorunsCommentsMode?: boolean;
showAssistantConnectionMode?: boolean;
showAssistantPromptMode?: boolean;
showAssistantChatMode?: boolean;
showAssistantSamMode?: boolean;
showDecompositionConnectionMode?: boolean;
showDecompositionPromptMode?: boolean;
showDecompositionQueryMode?: boolean;
showDecompositionOutputMode?: boolean;
showDecompositionMetricsMode?: boolean;
showDecompositionHistoryMode?: boolean;
showDecompositionRuntimeMode?: boolean;
prompts?: PromptState;
};
if (parsed.uiMode === "assistant" || parsed.uiMode === "decomposition" || parsed.uiMode === "autoruns") {
setUiMode(parsed.uiMode);
}
if (parsed.activeTab && TAB_KEYS.includes(parsed.activeTab)) {
setActiveTab(parsed.activeTab);
}
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.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 refreshHistory();
void refreshPresets(); void refreshPresets();
void refreshRuns(); void refreshRuns();
@ -159,6 +264,10 @@ export default function App() {
const payload = await apiClient.loadPresets(); const payload = await apiClient.loadPresets();
const presets = payload.presets ?? []; const presets = payload.presets ?? [];
setPresetList(presets); setPresetList(presets);
if (skipPresetAutoloadRef.current) {
presetAutoloadDoneRef.current = true;
return;
}
if (presetAutoloadDoneRef.current) { if (presetAutoloadDoneRef.current) {
return; return;
} }
@ -209,6 +318,34 @@ export default function App() {
log("Local config saved (without API key)."); log("Local config saved (without API key).");
} }
function saveAutorunsLayout() {
localStorage.setItem(
AUTORUNS_LAYOUT_CONFIG_KEY,
JSON.stringify({
uiMode,
activeTab,
showAutorunsAssistantMode,
showAutorunsDecompositionMode,
showAutorunsProgressMode,
showAutorunsCommentsMode,
showAssistantConnectionMode,
showAssistantPromptMode,
showAssistantChatMode,
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() { async function testConnection() {
setBusy(true); setBusy(true);
setLastError(""); setLastError("");
@ -522,10 +659,6 @@ export default function App() {
userMessage, userMessage,
sessionId: assistantSessionId || undefined, sessionId: assistantSessionId || undefined,
promptVersion: ASSISTANT_PROMPT_VERSION, promptVersion: ASSISTANT_PROMPT_VERSION,
context: {
periodHint: query.periodHint,
businessContext: query.businessContext
},
useMock useMock
}); });
setAssistantSessionId(response.session_id); setAssistantSessionId(response.session_id);
@ -555,7 +688,11 @@ export default function App() {
}, [selectedRunId]); }, [selectedRunId]);
return ( return (
<main className={`app-root ${uiMode === "autoruns" ? "app-root-autoruns" : ""}`}> <main
className={`app-root ${
uiMode === "assistant" || uiMode === "decomposition" || uiMode === "autoruns" ? "app-root-autoruns" : ""
}`}
>
<header className="app-topbar"> <header className="app-topbar">
<div className="mode-switch-row"> <div className="mode-switch-row">
<button type="button" className={uiMode === "assistant" ? "tab active" : "tab"} onClick={() => setUiMode("assistant")}> <button type="button" className={uiMode === "assistant" ? "tab active" : "tab"} onClick={() => setUiMode("assistant")}>
@ -567,9 +704,79 @@ export default function App() {
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}> <button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
История автопрогонов История автопрогонов
</button> </button>
<button type="button" className="tab" onClick={saveAutorunsLayout}>
Сохранить
</button>
</div> </div>
{uiMode === "autoruns" ? ( {uiMode === "assistant" ? (
<div className="mode-switch-row mode-switch-row-right">
<button
type="button"
className={showAssistantConnectionMode ? "tab active" : "tab"}
onClick={() => setShowAssistantConnectionMode((prev) => !prev)}
>
LLM Connector
</button>
<button
type="button"
className={showAssistantPromptMode ? "tab active" : "tab"}
onClick={() => setShowAssistantPromptMode((prev) => !prev)}
>
Prompt Manager
</button>
<button type="button" className={showAssistantChatMode ? "tab active" : "tab"} onClick={() => setShowAssistantChatMode((prev) => !prev)}>
Режим ассистента
</button>
<button type="button" className={showAssistantSamMode ? "tab active" : "tab"} onClick={() => setShowAssistantSamMode((prev) => !prev)}>
SAM
</button>
</div>
) : uiMode === "decomposition" ? (
<div className="mode-switch-row mode-switch-row-right">
<button
type="button"
className={showDecompositionConnectionMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionConnectionMode((prev) => !prev)}
>
LLM
</button>
<button
type="button"
className={showDecompositionPromptMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionPromptMode((prev) => !prev)}
>
Prompt
</button>
<button type="button" className={showDecompositionQueryMode ? "tab active" : "tab"} onClick={() => setShowDecompositionQueryMode((prev) => !prev)}>
Запрос
</button>
<button type="button" className={showDecompositionOutputMode ? "tab active" : "tab"} onClick={() => setShowDecompositionOutputMode((prev) => !prev)}>
Выход
</button>
<button
type="button"
className={showDecompositionMetricsMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionMetricsMode((prev) => !prev)}
>
Метрики
</button>
<button
type="button"
className={showDecompositionHistoryMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionHistoryMode((prev) => !prev)}
>
История
</button>
<button
type="button"
className={showDecompositionRuntimeMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionRuntimeMode((prev) => !prev)}
>
NDC Run Monitor
</button>
</div>
) : uiMode === "autoruns" ? (
<div className="mode-switch-row mode-switch-row-right"> <div className="mode-switch-row mode-switch-row-right">
<button <button
type="button" type="button"
@ -592,12 +799,22 @@ export default function App() {
> >
Прогресс/регресс Прогресс/регресс
</button> </button>
<button
type="button"
className={showAutorunsCommentsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsCommentsMode((prev) => !prev)}
>
Комментарии
</button>
</div> </div>
) : null} ) : null}
</header> </header>
{uiMode === "assistant" ? ( {uiMode === "assistant" ? (
<div className="layout-grid"> <div className="layout-grid layout-grid-mode-columns">
<div className="mode-columns">
{showAssistantConnectionMode ? (
<div className="mode-col">
<ConnectionPanel <ConnectionPanel
value={connection} value={connection}
modelOptions={modelOptions} modelOptions={modelOptions}
@ -609,7 +826,11 @@ export default function App() {
lastStatus={connectionStatus} lastStatus={connectionStatus}
busy={busy || assistantBusy} busy={busy || assistantBusy}
/> />
</div>
) : null}
{showAssistantPromptMode ? (
<div className="mode-col mode-col-wide">
<PromptPanel <PromptPanel
value={prompts} value={prompts}
onChange={setPrompts} onChange={setPrompts}
@ -624,16 +845,16 @@ export default function App() {
onPresetNameChange={setPresetName} onPresetNameChange={setPresetName}
diffSummary={diffSummary} diffSummary={diffSummary}
/> />
</div>
) : null}
{showAssistantChatMode ? (
<div className="mode-col mode-col-xwide">
<AssistantPanel <AssistantPanel
sessionId={assistantSessionId} sessionId={assistantSessionId}
conversation={assistantConversation} conversation={assistantConversation}
inputValue={assistantInput} inputValue={assistantInput}
onInputChange={setAssistantInput} onInputChange={setAssistantInput}
periodHint={query.periodHint}
onPeriodHintChange={(value) => setQuery((prev) => ({ ...prev, periodHint: value }))}
businessContext={query.businessContext}
onBusinessContextChange={(value) => setQuery((prev) => ({ ...prev, businessContext: value }))}
useMock={useMock} useMock={useMock}
onUseMockChange={setUseMock} onUseMockChange={setUseMock}
onSend={sendAssistantMessage} onSend={sendAssistantMessage}
@ -643,8 +864,31 @@ export default function App() {
errorMessage={assistantError} errorMessage={assistantError}
/> />
</div> </div>
) : null}
{showAssistantSamMode ? (
<div className="mode-col">
<AssistantSamPanel
sessionId={assistantSessionId}
conversation={assistantConversation}
statusText={assistantStatus}
errorMessage={assistantError}
useMock={useMock}
appLogs={appLogs}
/>
</div>
) : null}
{!showAssistantConnectionMode && !showAssistantPromptMode && !showAssistantChatMode && !showAssistantSamMode ? (
<div className="mode-columns-empty">Все панели режима ассистента скрыты. Включите нужные блоки справа в шапке.</div>
) : null}
</div>
</div>
) : uiMode === "decomposition" ? ( ) : uiMode === "decomposition" ? (
<div className="layout-grid"> <div className="layout-grid layout-grid-mode-columns">
<div className="mode-columns">
{showDecompositionConnectionMode ? (
<div className="mode-col">
<ConnectionPanel <ConnectionPanel
value={connection} value={connection}
modelOptions={modelOptions} modelOptions={modelOptions}
@ -656,7 +900,11 @@ export default function App() {
lastStatus={connectionStatus} lastStatus={connectionStatus}
busy={busy} busy={busy}
/> />
</div>
) : null}
{showDecompositionPromptMode ? (
<div className="mode-col mode-col-wide">
<PromptPanel <PromptPanel
value={prompts} value={prompts}
onChange={setPrompts} onChange={setPrompts}
@ -671,7 +919,11 @@ export default function App() {
onPresetNameChange={setPresetName} onPresetNameChange={setPresetName}
diffSummary={diffSummary} diffSummary={diffSummary}
/> />
</div>
) : null}
{showDecompositionQueryMode ? (
<div className="mode-col">
<QueryPanel <QueryPanel
value={query} value={query}
onChange={setQuery} onChange={setQuery}
@ -682,13 +934,29 @@ export default function App() {
onUseMockChange={setUseMock} onUseMockChange={setUseMock}
errorMessage={lastError} errorMessage={lastError}
/> />
</div>
) : null}
{showDecompositionOutputMode ? (
<div className="mode-col mode-col-xwide">
<OutputPanel tab={activeTab} onTabChange={setActiveTab} result={result} appLogs={appLogs} /> <OutputPanel tab={activeTab} onTabChange={setActiveTab} result={result} appLogs={appLogs} />
</div>
) : null}
{showDecompositionMetricsMode ? (
<div className="mode-col">
<MetricsPanel result={result} /> <MetricsPanel result={result} />
</div>
) : null}
{showDecompositionHistoryMode ? (
<div className="mode-col">
<HistoryPanel items={historyItems} onRefresh={refreshHistory} onOpenTrace={openTrace} /> <HistoryPanel items={historyItems} onRefresh={refreshHistory} onOpenTrace={openTrace} />
</div>
) : null}
{showDecompositionRuntimeMode ? (
<div className="mode-col mode-col-xwide">
<RuntimePanel <RuntimePanel
runs={runs} runs={runs}
selectedRunId={selectedRunId} selectedRunId={selectedRunId}
@ -703,6 +971,19 @@ export default function App() {
evalReport={evalReport} evalReport={evalReport}
/> />
</div> </div>
) : null}
{!showDecompositionConnectionMode &&
!showDecompositionPromptMode &&
!showDecompositionQueryMode &&
!showDecompositionOutputMode &&
!showDecompositionMetricsMode &&
!showDecompositionHistoryMode &&
!showDecompositionRuntimeMode ? (
<div className="mode-columns-empty">Все панели режима декомпозиции скрыты. Включите нужные блоки справа в шапке.</div>
) : null}
</div>
</div>
) : ( ) : (
<div className="layout-grid layout-grid-autoruns"> <div className="layout-grid layout-grid-autoruns">
<AutoRunsHistoryPanel <AutoRunsHistoryPanel
@ -713,6 +994,7 @@ export default function App() {
showAssistantMode={showAutorunsAssistantMode} showAssistantMode={showAutorunsAssistantMode}
showDecompositionMode={showAutorunsDecompositionMode} showDecompositionMode={showAutorunsDecompositionMode}
showProgressMode={showAutorunsProgressMode} showProgressMode={showAutorunsProgressMode}
showCommentsMode={showAutorunsCommentsMode}
onLog={log} onLog={log}
/> />
</div> </div>
@ -720,4 +1002,3 @@ export default function App() {
</main> </main>
); );
} }

View File

@ -371,6 +371,8 @@ export const apiClient = {
assistant_prompt_version?: string; assistant_prompt_version?: string;
decomposition_prompt_version?: string; decomposition_prompt_version?: string;
prompt_fingerprint?: string; prompt_fingerprint?: string;
autogen_personality_id?: string;
autogen_personality_prompt?: string;
}; };
}): Promise<{ ok: boolean; generation: { generation_id: string; created_at: string; mode: AutoGenMode; count: number; domain: string | null; questions: string[]; generated_by: string | null; saved_case_set_file: string | null; context: Record<string, unknown> | null } }> { }): Promise<{ ok: boolean; generation: { generation_id: string; created_at: string; mode: AutoGenMode; count: number; domain: string | null; questions: string[]; generated_by: string | null; saved_case_set_file: string | null; context: Record<string, unknown> | null } }> {
return request("/autoruns/autogen/generate", { return request("/autoruns/autogen/generate", {

View File

@ -9,10 +9,6 @@ interface AssistantPanelProps {
conversation: AssistantConversationItem[]; conversation: AssistantConversationItem[];
inputValue: string; inputValue: string;
onInputChange: (value: string) => void; onInputChange: (value: string) => void;
periodHint: string;
onPeriodHintChange: (value: string) => void;
businessContext: string;
onBusinessContextChange: (value: string) => void;
useMock: boolean; useMock: boolean;
onUseMockChange: (value: boolean) => void; onUseMockChange: (value: boolean) => void;
onSend: () => Promise<void> | void; onSend: () => Promise<void> | void;
@ -70,10 +66,6 @@ export function AssistantPanel({
conversation, conversation,
inputValue, inputValue,
onInputChange, onInputChange,
periodHint,
onPeriodHintChange,
businessContext,
onBusinessContextChange,
useMock, useMock,
onUseMockChange, onUseMockChange,
onSend, onSend,
@ -175,16 +167,6 @@ export function AssistantPanel({
</div> </div>
<div className="assistant-compose"> <div className="assistant-compose">
<div className="grid-two">
<label>
Подсказка по периоду
<input value={periodHint} onChange={(event) => onPeriodHintChange(event.target.value)} />
</label>
<label>
Бизнес-контекст
<input value={businessContext} onChange={(event) => onBusinessContextChange(event.target.value)} />
</label>
</div>
<label className="full-width"> <label className="full-width">
Сообщение Сообщение
<textarea <textarea

View File

@ -0,0 +1,64 @@
import type { AssistantConversationItem } from "../state/types";
import { JsonView } from "./JsonView";
import { PanelFrame } from "./PanelFrame";
interface AssistantSamPanelProps {
sessionId: string;
conversation: AssistantConversationItem[];
statusText: string;
errorMessage: string;
useMock: boolean;
appLogs: string[];
}
function formatDateTime(iso: string): string {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) {
return iso;
}
return date.toLocaleString("ru-RU");
}
export function AssistantSamPanel({ sessionId, conversation, statusText, errorMessage, useMock, appLogs }: AssistantSamPanelProps) {
const assistantReplies = conversation.filter((item) => item.role === "assistant").length;
const userMessages = conversation.filter((item) => item.role === "user").length;
const lastMessage = conversation.length > 0 ? conversation[conversation.length - 1] : null;
return (
<PanelFrame title="SAM" subtitle="System Assistant Monitor: срез по текущей сессии и логам.">
<div className="metrics-grid">
<div>
<span>session_id</span>
<strong>{sessionId || "новая сессия"}</strong>
</div>
<div>
<span>mock_mode</span>
<strong>{useMock ? "on" : "off"}</strong>
</div>
<div>
<span>сообщений пользователя</span>
<strong>{userMessages}</strong>
</div>
<div>
<span>ответов ассистента</span>
<strong>{assistantReplies}</strong>
</div>
<div>
<span>статус</span>
<strong>{statusText || "нет данных"}</strong>
</div>
<div>
<span>ошибка</span>
<strong>{errorMessage || "нет"}</strong>
</div>
<div>
<span>последнее сообщение</span>
<strong>{lastMessage?.created_at ? formatDateTime(lastMessage.created_at) : "нет данных"}</strong>
</div>
</div>
<h3 style={{ marginTop: 12 }}>Последние системные логи</h3>
<JsonView value={appLogs.slice(0, 120)} />
</PanelFrame>
);
}

View File

@ -28,11 +28,19 @@ interface AutoRunsHistoryPanelProps {
showAssistantMode: boolean; showAssistantMode: boolean;
showDecompositionMode: boolean; showDecompositionMode: boolean;
showProgressMode: boolean; showProgressMode: boolean;
showCommentsMode: boolean;
onLog?: (message: string) => void; onLog?: (message: string) => void;
} }
type UseMockFilter = "any" | "true" | "false"; type UseMockFilter = "any" | "true" | "false";
type LeftTabMode = "settings" | "comments"; type AutoGenPersonalityId = "general" | "settlements_60_62" | "month_close_costs_20_44" | "vat_document_register_book";
interface AutoGenPersonalityDefinition {
id: AutoGenPersonalityId;
label: string;
domain: string;
defaultPrompt: string;
}
interface AutoRunsFilters { interface AutoRunsFilters {
fromLocal: string; fromLocal: string;
@ -58,7 +66,8 @@ interface CommentModalState {
interface AutoGenSettingsState { interface AutoGenSettingsState {
mode: AutoGenMode; mode: AutoGenMode;
count: number; count: number;
domain: string; personalityId: AutoGenPersonalityId;
personalityPrompts: Record<AutoGenPersonalityId, string>;
persistToEvalCases: boolean; persistToEvalCases: boolean;
generatedBy: string; generatedBy: string;
} }
@ -75,10 +84,67 @@ const DEFAULT_FILTERS: AutoRunsFilters = {
const DEFAULT_MANUAL_DECISION: ManualCaseDecision = "needs_dialog_policy_fix"; const DEFAULT_MANUAL_DECISION: ManualCaseDecision = "needs_dialog_policy_fix";
const AUTORUNS_UI_CONFIG_KEY = "ndc_autoruns_ui_config_v1";
const AUTORUNS_SAVE_EVENT = "ndc-autoruns-save";
const AUTOGEN_PERSONALITIES: AutoGenPersonalityDefinition[] = [
{
id: "general",
label: "Общий контур",
domain: "",
defaultPrompt:
"Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл."
},
{
id: "settlements_60_62",
label: "Расчеты 60/62",
domain: "settlements_60_62",
defaultPrompt:
"Генерируй вопросы по расчетам с контрагентами (счета 60/62): закрытие задолженности, авансы, сверки, переносы остатков, цепочки документов."
},
{
id: "month_close_costs_20_44",
label: "Закрытие месяца 20/44",
domain: "month_close_costs_20_44",
defaultPrompt:
"Генерируй вопросы по закрытию месяца и затратам на счетах 20/44: распределение, закрытие, остатки, аномалии и разницы по периодам."
},
{
id: "vat_document_register_book",
label: "НДС и регистры",
domain: "vat_document_register_book",
defaultPrompt:
"Генерируй вопросы по НДС: начисление, вычет, книги покупок/продаж, счета-фактуры, прогноз обязательств и сверка цепочки документов."
}
];
function buildDefaultPersonalityPrompts(): Record<AutoGenPersonalityId, string> {
return AUTOGEN_PERSONALITIES.reduce((acc, item) => {
acc[item.id] = item.defaultPrompt;
return acc;
}, {} as Record<AutoGenPersonalityId, string>);
}
const AUTOGEN_PERSONALITY_IDS = new Set<AutoGenPersonalityId>(AUTOGEN_PERSONALITIES.map((item) => item.id));
interface AutoRunsUiConfig {
filters?: Partial<AutoRunsFilters>;
autoGenSettings?: {
mode?: AutoGenMode;
count?: number;
personalityId?: AutoGenPersonalityId;
personalityPrompts?: Partial<Record<AutoGenPersonalityId, string>>;
persistToEvalCases?: boolean;
generatedBy?: string;
};
annotationDecisionFilter?: ManualCaseDecision | "all";
}
const DEFAULT_AUTOGEN_SETTINGS: AutoGenSettingsState = { const DEFAULT_AUTOGEN_SETTINGS: AutoGenSettingsState = {
mode: "codex_creative", mode: "codex_creative",
count: 24, count: 24,
domain: "", personalityId: "general",
personalityPrompts: buildDefaultPersonalityPrompts(),
persistToEvalCases: true, persistToEvalCases: true,
generatedBy: "manual_reviewer" generatedBy: "manual_reviewer"
}; };
@ -179,13 +245,13 @@ export function AutoRunsHistoryPanel({
showAssistantMode, showAssistantMode,
showDecompositionMode, showDecompositionMode,
showProgressMode, showProgressMode,
showCommentsMode,
onLog onLog
}: AutoRunsHistoryPanelProps) { }: AutoRunsHistoryPanelProps) {
const [filters, setFilters] = useState<AutoRunsFilters>({ const [filters, setFilters] = useState<AutoRunsFilters>({
...DEFAULT_FILTERS, ...DEFAULT_FILTERS,
fromLocal: defaultFromDateValue() fromLocal: defaultFromDateValue()
}); });
const [leftTab, setLeftTab] = useState<LeftTabMode>("settings");
const [history, setHistory] = useState<AutoRunHistoryResponse | null>(null); const [history, setHistory] = useState<AutoRunHistoryResponse | null>(null);
const [runDetail, setRunDetail] = useState<AutoRunDetailResponse | null>(null); const [runDetail, setRunDetail] = useState<AutoRunDetailResponse | null>(null);
const [dialog, setDialog] = useState<AutoRunDialogResponse | null>(null); const [dialog, setDialog] = useState<AutoRunDialogResponse | null>(null);
@ -219,6 +285,10 @@ export function AutoRunsHistoryPanel({
}); });
const initialLoadDoneRef = useRef(false); const initialLoadDoneRef = useRef(false);
const selectedPersonality = useMemo(
() => AUTOGEN_PERSONALITIES.find((item) => item.id === autoGenSettings.personalityId) ?? AUTOGEN_PERSONALITIES[0],
[autoGenSettings.personalityId]
);
const activeRunSummary: AutoRunSummary | null = const activeRunSummary: AutoRunSummary | null =
history?.items.find((item) => item.run_id === selectedRunId) ?? runDetail?.run ?? null; history?.items.find((item) => item.run_id === selectedRunId) ?? runDetail?.run ?? null;
@ -308,6 +378,7 @@ export function AutoRunsHistoryPanel({
setAutoGenBusy(true); setAutoGenBusy(true);
setErrorText(""); setErrorText("");
try { try {
const activePersonalityPrompt = autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? "";
const promptFingerprint = [ const promptFingerprint = [
prompts.systemPrompt, prompts.systemPrompt,
prompts.developerPrompt, prompts.developerPrompt,
@ -320,7 +391,7 @@ export function AutoRunsHistoryPanel({
const payload = await apiClient.generateAutoRunQuestions({ const payload = await apiClient.generateAutoRunQuestions({
mode: autoGenSettings.mode, mode: autoGenSettings.mode,
count: autoGenSettings.count, count: autoGenSettings.count,
domain: autoGenSettings.domain.trim() || undefined, domain: selectedPersonality.domain || undefined,
persist_to_eval_cases: autoGenSettings.persistToEvalCases, persist_to_eval_cases: autoGenSettings.persistToEvalCases,
generated_by: autoGenSettings.generatedBy.trim() || undefined, generated_by: autoGenSettings.generatedBy.trim() || undefined,
context: { context: {
@ -328,7 +399,9 @@ export function AutoRunsHistoryPanel({
model: connection.model, model: connection.model,
assistant_prompt_version: assistantPromptVersion, assistant_prompt_version: assistantPromptVersion,
decomposition_prompt_version: decompositionPromptVersion, decomposition_prompt_version: decompositionPromptVersion,
prompt_fingerprint: promptFingerprint prompt_fingerprint: promptFingerprint,
autogen_personality_id: selectedPersonality.id,
autogen_personality_prompt: activePersonalityPrompt.trim() || undefined
} }
}); });
log( log(
@ -346,9 +419,10 @@ export function AutoRunsHistoryPanel({
}, [ }, [
assistantPromptVersion, assistantPromptVersion,
autoGenSettings.count, autoGenSettings.count,
autoGenSettings.domain,
autoGenSettings.generatedBy, autoGenSettings.generatedBy,
autoGenSettings.mode, autoGenSettings.mode,
autoGenSettings.personalityId,
autoGenSettings.personalityPrompts,
autoGenSettings.persistToEvalCases, autoGenSettings.persistToEvalCases,
connection.llmProvider, connection.llmProvider,
connection.model, connection.model,
@ -359,7 +433,9 @@ export function AutoRunsHistoryPanel({
prompts.domainPrompt, prompts.domainPrompt,
prompts.fewShotExamples, prompts.fewShotExamples,
prompts.schemaNotes, prompts.schemaNotes,
prompts.systemPrompt prompts.systemPrompt,
selectedPersonality.domain,
selectedPersonality.id
]); ]);
const loadCaseDialog = useCallback( const loadCaseDialog = useCallback(
@ -538,7 +614,6 @@ export function AutoRunsHistoryPanel({
const openAnnotationContext = useCallback( const openAnnotationContext = useCallback(
async (annotation: AutoRunAnnotationRecord) => { async (annotation: AutoRunAnnotationRecord) => {
setSelectedAnnotationId(annotation.annotation_id); setSelectedAnnotationId(annotation.annotation_id);
setLeftTab("settings");
await loadRunDetail(annotation.run_id, annotation.case_id); await loadRunDetail(annotation.run_id, annotation.case_id);
if (!history?.items.some((item) => item.run_id === annotation.run_id)) { if (!history?.items.some((item) => item.run_id === annotation.run_id)) {
setErrorText("Комментарий относится к прогону вне текущего фильтра. Детали загружены напрямую."); setErrorText("Комментарий относится к прогону вне текущего фильтра. Детали загружены напрямую.");
@ -560,6 +635,95 @@ export function AutoRunsHistoryPanel({
void loadAnnotations(); void loadAnnotations();
}, [annotationDecisionFilter, loadAnnotations]); }, [annotationDecisionFilter, loadAnnotations]);
useEffect(() => {
const raw = localStorage.getItem(AUTORUNS_UI_CONFIG_KEY);
if (!raw) return;
try {
const parsed = JSON.parse(raw) as AutoRunsUiConfig;
if (parsed.filters) {
const savedFilters = parsed.filters;
setFilters((prev) => ({
...prev,
...savedFilters,
limit: typeof savedFilters.limit === "number" ? Math.max(1, Math.min(500, savedFilters.limit)) : prev.limit
}));
}
if (parsed.autoGenSettings) {
setAutoGenSettings((prev) => {
const nextPrompts = {
...prev.personalityPrompts
};
for (const item of AUTOGEN_PERSONALITIES) {
const incoming = parsed.autoGenSettings?.personalityPrompts?.[item.id];
if (typeof incoming === "string") {
nextPrompts[item.id] = incoming;
}
}
const nextPersonalityId =
parsed.autoGenSettings?.personalityId && AUTOGEN_PERSONALITY_IDS.has(parsed.autoGenSettings.personalityId)
? parsed.autoGenSettings.personalityId
: prev.personalityId;
return {
...prev,
mode:
parsed.autoGenSettings?.mode === "codex_creative" || parsed.autoGenSettings?.mode === "qwen_seed"
? parsed.autoGenSettings.mode
: prev.mode,
count:
typeof parsed.autoGenSettings?.count === "number"
? Math.max(1, Math.min(200, parsed.autoGenSettings.count))
: prev.count,
personalityId: nextPersonalityId,
personalityPrompts: nextPrompts,
persistToEvalCases:
typeof parsed.autoGenSettings?.persistToEvalCases === "boolean"
? parsed.autoGenSettings.persistToEvalCases
: prev.persistToEvalCases,
generatedBy:
typeof parsed.autoGenSettings?.generatedBy === "string"
? parsed.autoGenSettings.generatedBy
: prev.generatedBy
};
});
}
if (
parsed.annotationDecisionFilter === "all" ||
(typeof parsed.annotationDecisionFilter === "string" && parsed.annotationDecisionFilter.length > 0)
) {
setAnnotationDecisionFilter(parsed.annotationDecisionFilter as ManualCaseDecision | "all");
}
} catch {
// ignore broken local cache
}
}, []);
const saveUiConfig = useCallback(() => {
const payload: AutoRunsUiConfig = {
filters,
autoGenSettings: {
mode: autoGenSettings.mode,
count: autoGenSettings.count,
personalityId: autoGenSettings.personalityId,
personalityPrompts: autoGenSettings.personalityPrompts,
persistToEvalCases: autoGenSettings.persistToEvalCases,
generatedBy: autoGenSettings.generatedBy
},
annotationDecisionFilter
};
localStorage.setItem(AUTORUNS_UI_CONFIG_KEY, JSON.stringify(payload));
}, [annotationDecisionFilter, autoGenSettings, filters]);
useEffect(() => {
const onSave = () => {
saveUiConfig();
log("Сохранены настройки панели автопрогонов.");
};
window.addEventListener(AUTORUNS_SAVE_EVENT, onSave as EventListener);
return () => {
window.removeEventListener(AUTORUNS_SAVE_EVENT, onSave as EventListener);
};
}, [log, saveUiConfig]);
return ( return (
<PanelFrame <PanelFrame
className="autoruns-frame" className="autoruns-frame"
@ -569,19 +733,9 @@ export function AutoRunsHistoryPanel({
<div className="autoruns-columns"> <div className="autoruns-columns">
<section className="autoruns-col"> <section className="autoruns-col">
<div className="autoruns-col-header"> <div className="autoruns-col-header">
<h3>Левая панель</h3> <h3>Настройки</h3>
<div className="tab-row">
<button type="button" className={leftTab === "settings" ? "tab active" : "tab"} onClick={() => setLeftTab("settings")}>
Настройки
</button>
<button type="button" className={leftTab === "comments" ? "tab active" : "tab"} onClick={() => setLeftTab("comments")}>
Комментарии
</button>
</div>
</div> </div>
{leftTab === "settings" ? (
<>
<h4>Настройки выборки</h4> <h4>Настройки выборки</h4>
<div className="autoruns-form-grid"> <div className="autoruns-form-grid">
<label> <label>
@ -726,12 +880,22 @@ export function AutoRunsHistoryPanel({
/> />
</label> </label>
<label> <label>
Домен (опц.) Личность автогенерации
<input <select
value={autoGenSettings.domain} value={autoGenSettings.personalityId}
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, domain: event.target.value }))} onChange={(event) =>
placeholder="vat / settlements / counterparties" setAutoGenSettings((prev) => ({
/> ...prev,
personalityId: event.target.value as AutoGenPersonalityId
}))
}
>
{AUTOGEN_PERSONALITIES.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
</select>
</label> </label>
<label> <label>
Кто генерирует Кто генерирует
@ -741,6 +905,22 @@ export function AutoRunsHistoryPanel({
placeholder="manual_reviewer" placeholder="manual_reviewer"
/> />
</label> </label>
<label className="full-width">
Промпт личности
<textarea
value={autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? ""}
onChange={(event) =>
setAutoGenSettings((prev) => ({
...prev,
personalityPrompts: {
...prev.personalityPrompts,
[prev.personalityId]: event.target.value
}
}))
}
placeholder="Текст промпта для выбранной личности автогенерации"
/>
</label>
<label className="checkbox-row"> <label className="checkbox-row">
<input <input
type="checkbox" type="checkbox"
@ -805,133 +985,6 @@ export function AutoRunsHistoryPanel({
<textarea readOnly value={prompts.fewShotExamples} /> <textarea readOnly value={prompts.fewShotExamples} />
</label> </label>
</details> </details>
</>
) : (
<>
<h4>Размеченные ответы</h4>
<div className="autoruns-form-grid">
<label>
Фильтр решений
<select
value={annotationDecisionFilter}
onChange={(event) => setAnnotationDecisionFilter(event.target.value as ManualCaseDecision | "all")}
>
<option value="all">все</option>
{(availableManualDecisions.length > 0
? availableManualDecisions
: ((manualDecisionSchema?.enum as ManualCaseDecision[] | undefined) ?? [])
).map((decision) => (
<option key={decision} value={decision}>
{String(((manualDecisionSchema?.labels as Record<string, unknown> | undefined)?.[decision] ?? decision))}
</option>
))}
</select>
</label>
</div>
<div className="autoruns-stats-grid">
<div>
<span>Комментариев</span>
<strong>{annotations.length}</strong>
</div>
<div>
<span>Средний рейтинг</span>
<strong>{annotationsAverageRating === null ? "нет данных" : `${annotationsAverageRating.toFixed(2)} / 5`}</strong>
</div>
<div>
<span>Последний</span>
<strong>{annotations.length > 0 ? formatDateTime(annotations[0].updated_at) : "нет данных"}</strong>
</div>
<div>
<span>Статус</span>
<strong>{annotationsBusy ? "обновляю" : "готово"}</strong>
</div>
</div>
<div className="button-row">
<button type="button" disabled={annotationsBusy} onClick={() => void loadAnnotations()}>
{annotationsBusy ? "Обновляю..." : "Обновить список"}
</button>
<button type="button" className="tab" disabled={postAnalysisBusy} onClick={() => void loadPostAnalysis()}>
{postAnalysisBusy ? "Идет пост-анализ..." : "Обновить пост-анализ"}
</button>
</div>
<div className="autoruns-comments-list">
{annotationsBusy ? <p className="muted">Загружаю комментарии...</p> : null}
{!annotationsBusy && annotations.length === 0 ? <p className="muted">Пока нет откомментированных ответов.</p> : null}
{annotations.map((item) => (
<button
key={item.annotation_id}
type="button"
className={selectedAnnotationId === item.annotation_id ? "autoruns-comment-item selected" : "autoruns-comment-item"}
onClick={() => void openAnnotationContext(item)}
>
<div className="autoruns-comment-head">
<strong>{renderRatingDots(item.rating)}</strong>
<span>{formatDateTime(item.updated_at)}</span>
</div>
<div className="autoruns-run-meta">{item.run_id}</div>
<div className="autoruns-run-meta">
case={item.case_id} | msg={item.message_index}
</div>
<div className="autoruns-run-meta">
decision={item.manual_case_decision}
{item.annotation_author ? ` | author=${item.annotation_author}` : ""}
</div>
<p>{item.comment}</p>
</button>
))}
</div>
{selectedAnnotation ? (
<>
<h4>Тех-контекст брака</h4>
<div className="autoruns-meta-list">
<div>
<span>trace:</span>
<strong>{selectedAnnotation.technical_context.trace_id ?? "нет данных"}</strong>
</div>
<div>
<span>reply_type:</span>
<strong>{selectedAnnotation.technical_context.reply_type ?? "нет данных"}</strong>
</div>
<div>
<span>domain:</span>
<strong>{selectedAnnotation.technical_context.domain ?? "нет данных"}</strong>
</div>
<div>
<span>query_class:</span>
<strong>{selectedAnnotation.technical_context.query_class ?? "нет данных"}</strong>
</div>
</div>
<h4>JSON разбор</h4>
<JsonView
value={{
annotation_id: selectedAnnotation.annotation_id,
run_id: selectedAnnotation.run_id,
case_id: selectedAnnotation.case_id,
message_index: selectedAnnotation.message_index,
rating: selectedAnnotation.rating,
comment: selectedAnnotation.comment,
manual_case_decision: selectedAnnotation.manual_case_decision,
annotation_author: selectedAnnotation.annotation_author,
context: selectedAnnotation.context,
technical_context: selectedAnnotation.technical_context,
case_summary: selectedAnnotation.case_summary
? {
case_id: selectedAnnotation.case_summary.case_id,
domain: selectedAnnotation.case_summary.domain,
query_class: selectedAnnotation.case_summary.query_class,
checks: selectedAnnotation.case_summary.checks,
metric_subscores: selectedAnnotation.case_summary.metric_subscores
}
: null
}}
/>
</>
) : null}
</>
)}
{errorText ? <p className="error-text">{errorText}</p> : null} {errorText ? <p className="error-text">{errorText}</p> : null}
</section> </section>
@ -1235,6 +1288,137 @@ export function AutoRunsHistoryPanel({
</div> </div>
</section> </section>
) : null} ) : null}
{showCommentsMode ? (
<section className="autoruns-col">
<div className="autoruns-col-header">
<h3>Комментарии</h3>
</div>
<h4>Размеченные ответы</h4>
<div className="autoruns-form-grid">
<label>
Фильтр решений
<select
value={annotationDecisionFilter}
onChange={(event) => setAnnotationDecisionFilter(event.target.value as ManualCaseDecision | "all")}
>
<option value="all">все</option>
{(availableManualDecisions.length > 0
? availableManualDecisions
: ((manualDecisionSchema?.enum as ManualCaseDecision[] | undefined) ?? [])
).map((decision) => (
<option key={decision} value={decision}>
{String(((manualDecisionSchema?.labels as Record<string, unknown> | undefined)?.[decision] ?? decision))}
</option>
))}
</select>
</label>
</div>
<div className="autoruns-stats-grid">
<div>
<span>Комментариев</span>
<strong>{annotations.length}</strong>
</div>
<div>
<span>Средний рейтинг</span>
<strong>{annotationsAverageRating === null ? "нет данных" : `${annotationsAverageRating.toFixed(2)} / 5`}</strong>
</div>
<div>
<span>Последний</span>
<strong>{annotations.length > 0 ? formatDateTime(annotations[0].updated_at) : "нет данных"}</strong>
</div>
<div>
<span>Статус</span>
<strong>{annotationsBusy ? "обновляю" : "готово"}</strong>
</div>
</div>
<div className="button-row">
<button type="button" disabled={annotationsBusy} onClick={() => void loadAnnotations()}>
{annotationsBusy ? "Обновляю..." : "Обновить список"}
</button>
<button type="button" className="tab" disabled={postAnalysisBusy} onClick={() => void loadPostAnalysis()}>
{postAnalysisBusy ? "Идет пост-анализ..." : "Обновить пост-анализ"}
</button>
</div>
<div className="autoruns-comments-list">
{annotationsBusy ? <p className="muted">Загружаю комментарии...</p> : null}
{!annotationsBusy && annotations.length === 0 ? <p className="muted">Пока нет откомментированных ответов.</p> : null}
{annotations.map((item) => (
<button
key={item.annotation_id}
type="button"
className={selectedAnnotationId === item.annotation_id ? "autoruns-comment-item selected" : "autoruns-comment-item"}
onClick={() => void openAnnotationContext(item)}
>
<div className="autoruns-comment-head">
<strong>{renderRatingDots(item.rating)}</strong>
<span>{formatDateTime(item.updated_at)}</span>
</div>
<div className="autoruns-run-meta">{item.run_id}</div>
<div className="autoruns-run-meta">
case={item.case_id} | msg={item.message_index}
</div>
<div className="autoruns-run-meta">
decision={item.manual_case_decision}
{item.annotation_author ? ` | author=${item.annotation_author}` : ""}
</div>
<p>{item.comment}</p>
</button>
))}
</div>
{selectedAnnotation ? (
<>
<h4>Тех-контекст брака</h4>
<div className="autoruns-meta-list">
<div>
<span>trace:</span>
<strong>{selectedAnnotation.technical_context.trace_id ?? "нет данных"}</strong>
</div>
<div>
<span>reply_type:</span>
<strong>{selectedAnnotation.technical_context.reply_type ?? "нет данных"}</strong>
</div>
<div>
<span>domain:</span>
<strong>{selectedAnnotation.technical_context.domain ?? "нет данных"}</strong>
</div>
<div>
<span>query_class:</span>
<strong>{selectedAnnotation.technical_context.query_class ?? "нет данных"}</strong>
</div>
</div>
<h4>JSON разбор</h4>
<JsonView
value={{
annotation_id: selectedAnnotation.annotation_id,
run_id: selectedAnnotation.run_id,
case_id: selectedAnnotation.case_id,
message_index: selectedAnnotation.message_index,
rating: selectedAnnotation.rating,
comment: selectedAnnotation.comment,
manual_case_decision: selectedAnnotation.manual_case_decision,
annotation_author: selectedAnnotation.annotation_author,
context: selectedAnnotation.context,
technical_context: selectedAnnotation.technical_context,
case_summary: selectedAnnotation.case_summary
? {
case_id: selectedAnnotation.case_summary.case_id,
domain: selectedAnnotation.case_summary.domain,
query_class: selectedAnnotation.case_summary.query_class,
checks: selectedAnnotation.case_summary.checks,
metric_subscores: selectedAnnotation.case_summary.metric_subscores
}
: null
}}
/>
</>
) : null}
</section>
) : null}
</div> </div>
{commentModal.open ? ( {commentModal.open ? (

View File

@ -43,7 +43,7 @@ export function PromptPanel({
}: PromptPanelProps) { }: PromptPanelProps) {
return ( return (
<PanelFrame title="Prompt Manager" subtitle="Системный, developer и domain уровни управляются отдельно."> <PanelFrame title="Prompt Manager" subtitle="Системный, developer и domain уровни управляются отдельно.">
<div className="grid-two"> <div className="prompt-manager-grid">
<label> <label>
Системный prompt Системный prompt
<textarea <textarea

View File

@ -48,7 +48,7 @@ export function RuntimePanel({
{evalBusy ? "Идет eval v2.0.2..." : "Запустить eval v2.0.2"} {evalBusy ? "Идет eval v2.0.2..." : "Запустить eval v2.0.2"}
</button> </button>
</div> </div>
<div className="runtime-grid"> <div className="runtime-stack">
<div className="runtime-runs"> <div className="runtime-runs">
{runs.map((run) => ( {runs.map((run) => (
<button <button
@ -69,7 +69,7 @@ export function RuntimePanel({
))} ))}
{runs.length === 0 ? <p className="muted">Нет активных запусков.</p> : null} {runs.length === 0 ? <p className="muted">Нет активных запусков.</p> : null}
</div> </div>
<div> <div className="runtime-details">
<h3>Trace выбранного run</h3> <h3>Trace выбранного run</h3>
<JsonView value={traceItems} /> <JsonView value={traceItems} />
<div className="eval-report-wrap"> <div className="eval-report-wrap">

View File

@ -13,6 +13,8 @@
--rgb-scrollbar-track: 31, 31, 36; --rgb-scrollbar-track: 31, 31, 36;
--rgb-scrollbar-thumb: 74, 74, 82; --rgb-scrollbar-thumb: 74, 74, 82;
--rgb-scrollbar-thumb-hover: 90, 90, 100; --rgb-scrollbar-thumb-hover: 90, 90, 100;
--mode-column-width: 440px;
--mode-toggle-width: 188px;
--bg-main: rgb(var(--rgb-background)); --bg-main: rgb(var(--rgb-background));
--bg-soft: rgb(var(--rgb-surface-main)); --bg-soft: rgb(var(--rgb-surface-main));
--bg-panel: rgb(var(--rgb-surface-main)); --bg-panel: rgb(var(--rgb-surface-main));
@ -29,7 +31,7 @@
--radius-lg: 20px; --radius-lg: 20px;
--radius-md: 14px; --radius-md: 14px;
--shadow: none; --shadow: none;
--autoruns-col-width: 360px; --autoruns-col-width: var(--mode-column-width);
} }
* { * {
@ -105,6 +107,12 @@ body,
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
} }
.layout-grid.layout-grid-mode-columns {
min-height: 0;
flex: 1 1 auto;
grid-template-columns: minmax(0, 1fr);
}
.mode-switch-row { .mode-switch-row {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -115,6 +123,67 @@ body,
.mode-switch-row.mode-switch-row-right { .mode-switch-row.mode-switch-row-right {
margin-left: auto; margin-left: auto;
justify-content: flex-end; justify-content: flex-end;
max-width: 72%;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 2px;
}
.mode-switch-row .tab {
white-space: nowrap;
}
.mode-switch-row.mode-switch-row-right .tab {
width: var(--mode-toggle-width);
min-width: var(--mode-toggle-width);
text-align: center;
}
.mode-columns {
display: flex;
gap: 12px;
width: 100%;
min-height: 0;
flex: 1 1 auto;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 4px;
}
.mode-col {
flex: 0 0 var(--mode-column-width);
width: var(--mode-column-width);
min-height: 0;
height: 100%;
display: flex;
}
.mode-col.mode-col-wide {
flex-basis: var(--mode-column-width);
width: var(--mode-column-width);
}
.mode-col.mode-col-xwide {
flex-basis: var(--mode-column-width);
width: var(--mode-column-width);
}
.mode-col .panel-frame {
width: 100%;
height: 100%;
}
.mode-col .panel-body {
min-height: 0;
overflow: auto;
}
.mode-columns-empty {
min-width: 360px;
border-radius: 14px;
background: rgb(var(--rgb-surface-main));
color: var(--text-muted);
padding: 14px;
} }
.panel-frame { .panel-frame {
@ -303,6 +372,12 @@ button:disabled {
gap: 12px; gap: 12px;
} }
.prompt-manager-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
.full-width { .full-width {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
@ -478,6 +553,17 @@ button:disabled {
gap: 12px; gap: 12px;
} }
.runtime-stack {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
.runtime-details {
display: grid;
gap: 12px;
}
.runtime-runs { .runtime-runs {
max-height: 360px; max-height: 360px;
overflow: auto; overflow: auto;
@ -532,8 +618,8 @@ button:disabled {
} }
.autoruns-col { .autoruns-col {
flex: 0 0 var(--autoruns-col-width); flex: 0 0 var(--mode-column-width);
width: var(--autoruns-col-width); width: var(--mode-column-width);
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow: auto; overflow: auto;
@ -544,11 +630,6 @@ button:disabled {
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }
.autoruns-col:nth-child(3) {
flex-basis: 440px;
width: 440px;
}
.autoruns-col h3 { .autoruns-col h3 {
margin: 0; margin: 0;
font-size: 0.95rem; font-size: 0.95rem;
@ -990,7 +1071,7 @@ button:disabled {
@media (max-width: 1200px) { @media (max-width: 1200px) {
:root { :root {
--autoruns-col-width: 340px; --mode-column-width: 400px;
} }
.metrics-grid { .metrics-grid {
@ -1000,11 +1081,12 @@ button:disabled {
@media (max-width: 920px) { @media (max-width: 920px) {
:root { :root {
--autoruns-col-width: 320px; --mode-column-width: 360px;
} }
.grid-two, .grid-two,
.runtime-grid { .runtime-grid,
.runtime-stack {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@ -1021,7 +1103,7 @@ button:disabled {
@media (max-width: 640px) { @media (max-width: 640px) {
:root { :root {
--autoruns-col-width: 300px; --mode-column-width: 320px;
} }
.app-root { .app-root {

View File

@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/components/assistantpanel.tsx","./src/components/autorunshistorypanel.tsx","./src/components/connectionpanel.tsx","./src/components/historypanel.tsx","./src/components/jsonview.tsx","./src/components/metricspanel.tsx","./src/components/outputpanel.tsx","./src/components/panelframe.tsx","./src/components/promptpanel.tsx","./src/components/querypanel.tsx","./src/components/runtimepanel.tsx","./src/state/defaults.ts","./src/state/types.ts","./src/utils/conversationexport.ts"],"version":"5.9.3"} {"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/components/assistantpanel.tsx","./src/components/assistantsampanel.tsx","./src/components/autorunshistorypanel.tsx","./src/components/connectionpanel.tsx","./src/components/historypanel.tsx","./src/components/jsonview.tsx","./src/components/metricspanel.tsx","./src/components/outputpanel.tsx","./src/components/panelframe.tsx","./src/components/promptpanel.tsx","./src/components/querypanel.tsx","./src/components/runtimepanel.tsx","./src/state/defaults.ts","./src/state/types.ts","./src/utils/conversationexport.ts"],"version":"5.9.3"}