АДРЕСНЫЙ РЕЖИМ - авторан история - база + ДИЗАЙН
This commit is contained in:
+383
-102
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user