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

This commit is contained in:
2026-04-09 17:33:32 +03:00
parent 60a4bef88c
commit cf5ba1afc2
15 changed files with 904 additions and 305 deletions
+383 -102
View File
@@ -1,5 +1,6 @@
import { useEffect, 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";
@@ -24,10 +25,13 @@ import type {
} 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 = "assistant";
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}`;
@@ -83,6 +87,18 @@ export default function App() {
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 [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<AssistantConversationItem[]>([]);
const [assistantInput, setAssistantInput] = useState("");
@@ -90,6 +106,7 @@ export default function App() {
const [assistantStatus, setAssistantStatus] = useState("");
const [assistantError, setAssistantError] = useState("");
const presetAutoloadDoneRef = useRef(false);
const skipPresetAutoloadRef = useRef(false);
useEffect(() => {
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-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) => {
@@ -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 refreshPresets();
void refreshRuns();
@@ -159,6 +264,10 @@ export default function App() {
const payload = await apiClient.loadPresets();
const presets = payload.presets ?? [];
setPresetList(presets);
if (skipPresetAutoloadRef.current) {
presetAutoloadDoneRef.current = true;
return;
}
if (presetAutoloadDoneRef.current) {
return;
}
@@ -209,6 +318,34 @@ export default function App() {
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() {
setBusy(true);
setLastError("");
@@ -522,10 +659,6 @@ export default function App() {
userMessage,
sessionId: assistantSessionId || undefined,
promptVersion: ASSISTANT_PROMPT_VERSION,
context: {
periodHint: query.periodHint,
businessContext: query.businessContext
},
useMock
});
setAssistantSessionId(response.session_id);
@@ -555,7 +688,11 @@ export default function App() {
}, [selectedRunId]);
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">
<div className="mode-switch-row">
<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>
<button type="button" className="tab" onClick={saveAutorunsLayout}>
Сохранить
</button>
</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">
<button
type="button"
@@ -592,116 +799,190 @@ export default function App() {
>
Прогресс/регресс
</button>
<button
type="button"
className={showAutorunsCommentsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsCommentsMode((prev) => !prev)}
>
Комментарии
</button>
</div>
) : null}
</header>
{uiMode === "assistant" ? (
<div className="layout-grid">
<ConnectionPanel
value={connection}
modelOptions={modelOptions}
modelsBusy={modelsBusy}
onChange={setConnection}
onReloadModels={reloadModels}
onSaveLocalConfig={saveLocalConfig}
onTestConnection={testConnection}
lastStatus={connectionStatus}
busy={busy || assistantBusy}
/>
<div className="layout-grid layout-grid-mode-columns">
<div className="mode-columns">
{showAssistantConnectionMode ? (
<div className="mode-col">
<ConnectionPanel
value={connection}
modelOptions={modelOptions}
modelsBusy={modelsBusy}
onChange={setConnection}
onReloadModels={reloadModels}
onSaveLocalConfig={saveLocalConfig}
onTestConnection={testConnection}
lastStatus={connectionStatus}
busy={busy || assistantBusy}
/>
</div>
) : null}
<PromptPanel
value={prompts}
onChange={setPrompts}
presets={presetList}
selectedPresetId={selectedPresetId}
onSelectPreset={setSelectedPresetId}
onLoadPreset={loadSelectedPreset}
onSavePreset={savePreset}
onResetDefaults={resetDefaults}
onDiffPrevious={diffWithPrevious}
presetName={presetName}
onPresetNameChange={setPresetName}
diffSummary={diffSummary}
/>
{showAssistantPromptMode ? (
<div className="mode-col mode-col-wide">
<PromptPanel
value={prompts}
onChange={setPrompts}
presets={presetList}
selectedPresetId={selectedPresetId}
onSelectPreset={setSelectedPresetId}
onLoadPreset={loadSelectedPreset}
onSavePreset={savePreset}
onResetDefaults={resetDefaults}
onDiffPrevious={diffWithPrevious}
presetName={presetName}
onPresetNameChange={setPresetName}
diffSummary={diffSummary}
/>
</div>
) : null}
<AssistantPanel
sessionId={assistantSessionId}
conversation={assistantConversation}
inputValue={assistantInput}
onInputChange={setAssistantInput}
periodHint={query.periodHint}
onPeriodHintChange={(value) => setQuery((prev) => ({ ...prev, periodHint: value }))}
businessContext={query.businessContext}
onBusinessContextChange={(value) => setQuery((prev) => ({ ...prev, businessContext: value }))}
useMock={useMock}
onUseMockChange={setUseMock}
onSend={sendAssistantMessage}
onClear={resetAssistantSession}
busy={assistantBusy}
statusText={assistantStatus}
errorMessage={assistantError}
/>
{showAssistantChatMode ? (
<div className="mode-col mode-col-xwide">
<AssistantPanel
sessionId={assistantSessionId}
conversation={assistantConversation}
inputValue={assistantInput}
onInputChange={setAssistantInput}
useMock={useMock}
onUseMockChange={setUseMock}
onSend={sendAssistantMessage}
onClear={resetAssistantSession}
busy={assistantBusy}
statusText={assistantStatus}
errorMessage={assistantError}
/>
</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" ? (
<div className="layout-grid">
<ConnectionPanel
value={connection}
modelOptions={modelOptions}
modelsBusy={modelsBusy}
onChange={setConnection}
onReloadModels={reloadModels}
onSaveLocalConfig={saveLocalConfig}
onTestConnection={testConnection}
lastStatus={connectionStatus}
busy={busy}
/>
<div className="layout-grid layout-grid-mode-columns">
<div className="mode-columns">
{showDecompositionConnectionMode ? (
<div className="mode-col">
<ConnectionPanel
value={connection}
modelOptions={modelOptions}
modelsBusy={modelsBusy}
onChange={setConnection}
onReloadModels={reloadModels}
onSaveLocalConfig={saveLocalConfig}
onTestConnection={testConnection}
lastStatus={connectionStatus}
busy={busy}
/>
</div>
) : null}
<PromptPanel
value={prompts}
onChange={setPrompts}
presets={presetList}
selectedPresetId={selectedPresetId}
onSelectPreset={setSelectedPresetId}
onLoadPreset={loadSelectedPreset}
onSavePreset={savePreset}
onResetDefaults={resetDefaults}
onDiffPrevious={diffWithPrevious}
presetName={presetName}
onPresetNameChange={setPresetName}
diffSummary={diffSummary}
/>
{showDecompositionPromptMode ? (
<div className="mode-col mode-col-wide">
<PromptPanel
value={prompts}
onChange={setPrompts}
presets={presetList}
selectedPresetId={selectedPresetId}
onSelectPreset={setSelectedPresetId}
onLoadPreset={loadSelectedPreset}
onSavePreset={savePreset}
onResetDefaults={resetDefaults}
onDiffPrevious={diffWithPrevious}
presetName={presetName}
onPresetNameChange={setPresetName}
diffSummary={diffSummary}
/>
</div>
) : null}
<QueryPanel
value={query}
onChange={setQuery}
onApplyBatchFormat={applyBatchFormat}
onNormalize={normalize}
busy={busy}
useMock={useMock}
onUseMockChange={setUseMock}
errorMessage={lastError}
/>
{showDecompositionQueryMode ? (
<div className="mode-col">
<QueryPanel
value={query}
onChange={setQuery}
onApplyBatchFormat={applyBatchFormat}
onNormalize={normalize}
busy={busy}
useMock={useMock}
onUseMockChange={setUseMock}
errorMessage={lastError}
/>
</div>
) : null}
<OutputPanel tab={activeTab} onTabChange={setActiveTab} result={result} appLogs={appLogs} />
{showDecompositionOutputMode ? (
<div className="mode-col mode-col-xwide">
<OutputPanel tab={activeTab} onTabChange={setActiveTab} result={result} appLogs={appLogs} />
</div>
) : null}
<MetricsPanel result={result} />
{showDecompositionMetricsMode ? (
<div className="mode-col">
<MetricsPanel result={result} />
</div>
) : null}
<HistoryPanel items={historyItems} onRefresh={refreshHistory} onOpenTrace={openTrace} />
{showDecompositionHistoryMode ? (
<div className="mode-col">
<HistoryPanel items={historyItems} onRefresh={refreshHistory} onOpenTrace={openTrace} />
</div>
) : null}
<RuntimePanel
runs={runs}
selectedRunId={selectedRunId}
onSelectRun={setSelectedRunId}
onStartRun={startRun}
onFinishRun={finishRun}
onRefreshRuns={refreshRuns}
onRunEval={runEval}
onCopyEvalReport={copyEvalReport}
evalBusy={evalBusy}
traceItems={runTrace}
evalReport={evalReport}
/>
{showDecompositionRuntimeMode ? (
<div className="mode-col mode-col-xwide">
<RuntimePanel
runs={runs}
selectedRunId={selectedRunId}
onSelectRun={setSelectedRunId}
onStartRun={startRun}
onFinishRun={finishRun}
onRefreshRuns={refreshRuns}
onRunEval={runEval}
onCopyEvalReport={copyEvalReport}
evalBusy={evalBusy}
traceItems={runTrace}
evalReport={evalReport}
/>
</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">
@@ -713,6 +994,7 @@ export default function App() {
showAssistantMode={showAutorunsAssistantMode}
showDecompositionMode={showAutorunsDecompositionMode}
showProgressMode={showAutorunsProgressMode}
showCommentsMode={showAutorunsCommentsMode}
onLog={log}
/>
</div>
@@ -720,4 +1002,3 @@ export default function App() {
</main>
);
}
@@ -371,6 +371,8 @@ export const apiClient = {
assistant_prompt_version?: string;
decomposition_prompt_version?: 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 } }> {
return request("/autoruns/autogen/generate", {
@@ -9,10 +9,6 @@ interface AssistantPanelProps {
conversation: AssistantConversationItem[];
inputValue: string;
onInputChange: (value: string) => void;
periodHint: string;
onPeriodHintChange: (value: string) => void;
businessContext: string;
onBusinessContextChange: (value: string) => void;
useMock: boolean;
onUseMockChange: (value: boolean) => void;
onSend: () => Promise<void> | void;
@@ -70,10 +66,6 @@ export function AssistantPanel({
conversation,
inputValue,
onInputChange,
periodHint,
onPeriodHintChange,
businessContext,
onBusinessContextChange,
useMock,
onUseMockChange,
onSend,
@@ -175,16 +167,6 @@ export function AssistantPanel({
</div>
<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">
Сообщение
<textarea
@@ -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>
);
}
@@ -28,11 +28,19 @@ interface AutoRunsHistoryPanelProps {
showAssistantMode: boolean;
showDecompositionMode: boolean;
showProgressMode: boolean;
showCommentsMode: boolean;
onLog?: (message: string) => void;
}
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 {
fromLocal: string;
@@ -58,7 +66,8 @@ interface CommentModalState {
interface AutoGenSettingsState {
mode: AutoGenMode;
count: number;
domain: string;
personalityId: AutoGenPersonalityId;
personalityPrompts: Record<AutoGenPersonalityId, string>;
persistToEvalCases: boolean;
generatedBy: string;
}
@@ -75,10 +84,67 @@ const DEFAULT_FILTERS: AutoRunsFilters = {
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 = {
mode: "codex_creative",
count: 24,
domain: "",
personalityId: "general",
personalityPrompts: buildDefaultPersonalityPrompts(),
persistToEvalCases: true,
generatedBy: "manual_reviewer"
};
@@ -179,13 +245,13 @@ export function AutoRunsHistoryPanel({
showAssistantMode,
showDecompositionMode,
showProgressMode,
showCommentsMode,
onLog
}: AutoRunsHistoryPanelProps) {
const [filters, setFilters] = useState<AutoRunsFilters>({
...DEFAULT_FILTERS,
fromLocal: defaultFromDateValue()
});
const [leftTab, setLeftTab] = useState<LeftTabMode>("settings");
const [history, setHistory] = useState<AutoRunHistoryResponse | null>(null);
const [runDetail, setRunDetail] = useState<AutoRunDetailResponse | null>(null);
const [dialog, setDialog] = useState<AutoRunDialogResponse | null>(null);
@@ -219,6 +285,10 @@ export function AutoRunsHistoryPanel({
});
const initialLoadDoneRef = useRef(false);
const selectedPersonality = useMemo(
() => AUTOGEN_PERSONALITIES.find((item) => item.id === autoGenSettings.personalityId) ?? AUTOGEN_PERSONALITIES[0],
[autoGenSettings.personalityId]
);
const activeRunSummary: AutoRunSummary | null =
history?.items.find((item) => item.run_id === selectedRunId) ?? runDetail?.run ?? null;
@@ -308,6 +378,7 @@ export function AutoRunsHistoryPanel({
setAutoGenBusy(true);
setErrorText("");
try {
const activePersonalityPrompt = autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? "";
const promptFingerprint = [
prompts.systemPrompt,
prompts.developerPrompt,
@@ -320,7 +391,7 @@ export function AutoRunsHistoryPanel({
const payload = await apiClient.generateAutoRunQuestions({
mode: autoGenSettings.mode,
count: autoGenSettings.count,
domain: autoGenSettings.domain.trim() || undefined,
domain: selectedPersonality.domain || undefined,
persist_to_eval_cases: autoGenSettings.persistToEvalCases,
generated_by: autoGenSettings.generatedBy.trim() || undefined,
context: {
@@ -328,7 +399,9 @@ export function AutoRunsHistoryPanel({
model: connection.model,
assistant_prompt_version: assistantPromptVersion,
decomposition_prompt_version: decompositionPromptVersion,
prompt_fingerprint: promptFingerprint
prompt_fingerprint: promptFingerprint,
autogen_personality_id: selectedPersonality.id,
autogen_personality_prompt: activePersonalityPrompt.trim() || undefined
}
});
log(
@@ -346,9 +419,10 @@ export function AutoRunsHistoryPanel({
}, [
assistantPromptVersion,
autoGenSettings.count,
autoGenSettings.domain,
autoGenSettings.generatedBy,
autoGenSettings.mode,
autoGenSettings.personalityId,
autoGenSettings.personalityPrompts,
autoGenSettings.persistToEvalCases,
connection.llmProvider,
connection.model,
@@ -359,7 +433,9 @@ export function AutoRunsHistoryPanel({
prompts.domainPrompt,
prompts.fewShotExamples,
prompts.schemaNotes,
prompts.systemPrompt
prompts.systemPrompt,
selectedPersonality.domain,
selectedPersonality.id
]);
const loadCaseDialog = useCallback(
@@ -538,7 +614,6 @@ export function AutoRunsHistoryPanel({
const openAnnotationContext = useCallback(
async (annotation: AutoRunAnnotationRecord) => {
setSelectedAnnotationId(annotation.annotation_id);
setLeftTab("settings");
await loadRunDetail(annotation.run_id, annotation.case_id);
if (!history?.items.some((item) => item.run_id === annotation.run_id)) {
setErrorText("Комментарий относится к прогону вне текущего фильтра. Детали загружены напрямую.");
@@ -560,6 +635,95 @@ export function AutoRunsHistoryPanel({
void 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 (
<PanelFrame
className="autoruns-frame"
@@ -569,19 +733,9 @@ export function AutoRunsHistoryPanel({
<div className="autoruns-columns">
<section className="autoruns-col">
<div className="autoruns-col-header">
<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>
<h3>Настройки</h3>
</div>
{leftTab === "settings" ? (
<>
<h4>Настройки выборки</h4>
<div className="autoruns-form-grid">
<label>
@@ -726,12 +880,22 @@ export function AutoRunsHistoryPanel({
/>
</label>
<label>
Домен (опц.)
<input
value={autoGenSettings.domain}
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, domain: event.target.value }))}
placeholder="vat / settlements / counterparties"
/>
Личность автогенерации
<select
value={autoGenSettings.personalityId}
onChange={(event) =>
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>
Кто генерирует
@@ -741,6 +905,22 @@ export function AutoRunsHistoryPanel({
placeholder="manual_reviewer"
/>
</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">
<input
type="checkbox"
@@ -805,133 +985,6 @@ export function AutoRunsHistoryPanel({
<textarea readOnly value={prompts.fewShotExamples} />
</label>
</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}
</section>
@@ -1235,6 +1288,137 @@ export function AutoRunsHistoryPanel({
</div>
</section>
) : 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>
{commentModal.open ? (
@@ -43,7 +43,7 @@ export function PromptPanel({
}: PromptPanelProps) {
return (
<PanelFrame title="Prompt Manager" subtitle="Системный, developer и domain уровни управляются отдельно.">
<div className="grid-two">
<div className="prompt-manager-grid">
<label>
Системный prompt
<textarea
@@ -48,7 +48,7 @@ export function RuntimePanel({
{evalBusy ? "Идет eval v2.0.2..." : "Запустить eval v2.0.2"}
</button>
</div>
<div className="runtime-grid">
<div className="runtime-stack">
<div className="runtime-runs">
{runs.map((run) => (
<button
@@ -69,7 +69,7 @@ export function RuntimePanel({
))}
{runs.length === 0 ? <p className="muted">Нет активных запусков.</p> : null}
</div>
<div>
<div className="runtime-details">
<h3>Trace выбранного run</h3>
<JsonView value={traceItems} />
<div className="eval-report-wrap">
+94 -12
View File
@@ -13,6 +13,8 @@
--rgb-scrollbar-track: 31, 31, 36;
--rgb-scrollbar-thumb: 74, 74, 82;
--rgb-scrollbar-thumb-hover: 90, 90, 100;
--mode-column-width: 440px;
--mode-toggle-width: 188px;
--bg-main: rgb(var(--rgb-background));
--bg-soft: rgb(var(--rgb-surface-main));
--bg-panel: rgb(var(--rgb-surface-main));
@@ -29,7 +31,7 @@
--radius-lg: 20px;
--radius-md: 14px;
--shadow: none;
--autoruns-col-width: 360px;
--autoruns-col-width: var(--mode-column-width);
}
* {
@@ -105,6 +107,12 @@ body,
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 {
display: flex;
gap: 8px;
@@ -115,6 +123,67 @@ body,
.mode-switch-row.mode-switch-row-right {
margin-left: auto;
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 {
@@ -303,6 +372,12 @@ button:disabled {
gap: 12px;
}
.prompt-manager-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
.full-width {
grid-column: 1 / -1;
}
@@ -478,6 +553,17 @@ button:disabled {
gap: 12px;
}
.runtime-stack {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
.runtime-details {
display: grid;
gap: 12px;
}
.runtime-runs {
max-height: 360px;
overflow: auto;
@@ -532,8 +618,8 @@ button:disabled {
}
.autoruns-col {
flex: 0 0 var(--autoruns-col-width);
width: var(--autoruns-col-width);
flex: 0 0 var(--mode-column-width);
width: var(--mode-column-width);
height: 100%;
min-height: 0;
overflow: auto;
@@ -544,11 +630,6 @@ button:disabled {
scrollbar-gutter: stable;
}
.autoruns-col:nth-child(3) {
flex-basis: 440px;
width: 440px;
}
.autoruns-col h3 {
margin: 0;
font-size: 0.95rem;
@@ -990,7 +1071,7 @@ button:disabled {
@media (max-width: 1200px) {
:root {
--autoruns-col-width: 340px;
--mode-column-width: 400px;
}
.metrics-grid {
@@ -1000,11 +1081,12 @@ button:disabled {
@media (max-width: 920px) {
:root {
--autoruns-col-width: 320px;
--mode-column-width: 360px;
}
.grid-two,
.runtime-grid {
.runtime-grid,
.runtime-stack {
grid-template-columns: 1fr;
}
@@ -1021,7 +1103,7 @@ button:disabled {
@media (max-width: 640px) {
:root {
--autoruns-col-width: 300px;
--mode-column-width: 320px;
}
.app-root {