АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия
This commit is contained in:
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NDC AI Normalizer Playground</title>
|
||||
<script type="module" crossorigin src="/assets/index-BFy6DcyX.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Ch7jCAii.css">
|
||||
<script type="module" crossorigin src="/assets/index-D6Y_lHrc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BMWPMdQA.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { apiClient } from "./api/client";
|
||||
import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel";
|
||||
import { AssistantPanel } from "./components/AssistantPanel";
|
||||
import { ConnectionPanel } from "./components/ConnectionPanel";
|
||||
import { HistoryPanel } from "./components/HistoryPanel";
|
||||
@@ -22,7 +23,7 @@ import type {
|
||||
} from "./state/types";
|
||||
|
||||
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
|
||||
const ASSISTANT_STAGES = ["Разбираю запрос", "Ищу данные", "Собираю ответ"];
|
||||
const ASSISTANT_STAGES = ["Analyzing request", "Fetching data", "Composing answer"];
|
||||
const DEFAULT_UI_MODE: UiMode = "assistant";
|
||||
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
|
||||
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
|
||||
@@ -508,12 +509,12 @@ export default function App() {
|
||||
});
|
||||
setAssistantSessionId(response.session_id);
|
||||
setAssistantConversation(response.conversation);
|
||||
setAssistantStatus("Ответ готов");
|
||||
setAssistantStatus("Reply is ready");
|
||||
log(`Assistant reply received: trace=${response.debug.trace_id}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setAssistantError(message);
|
||||
setAssistantStatus("Ошибка ассистента");
|
||||
setAssistantStatus("Assistant error");
|
||||
log(`Assistant error: ${message}`);
|
||||
} finally {
|
||||
stopTicker();
|
||||
@@ -536,15 +537,18 @@ export default function App() {
|
||||
<main className="app-root">
|
||||
<div className="hero">
|
||||
<h1>NDC AI First Layer</h1>
|
||||
<p>Два режима в одном интерфейсе: диагностика декомпозиции и диалоговый ассистент на общем backend-контуре.</p>
|
||||
<p>Three modes in one UI: assistant, decomposition diagnostics, and auto-run history with regression visibility.</p>
|
||||
</div>
|
||||
|
||||
<div className="mode-switch-row">
|
||||
<button type="button" className={uiMode === "assistant" ? "tab active" : "tab"} onClick={() => setUiMode("assistant")}>
|
||||
Ассистент
|
||||
Assistant
|
||||
</button>
|
||||
<button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
|
||||
Декомпозиция
|
||||
Decomposition
|
||||
</button>
|
||||
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
|
||||
AutoRun History
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -595,7 +599,7 @@ export default function App() {
|
||||
errorMessage={assistantError}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
) : uiMode === "decomposition" ? (
|
||||
<div className="layout-grid">
|
||||
<ConnectionPanel
|
||||
value={connection}
|
||||
@@ -655,7 +659,18 @@ export default function App() {
|
||||
evalReport={evalReport}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="layout-grid">
|
||||
<AutoRunsHistoryPanel
|
||||
connection={connection}
|
||||
prompts={prompts}
|
||||
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
|
||||
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
|
||||
onLog={log}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type {
|
||||
AutoRunDetailResponse,
|
||||
AutoRunDialogResponse,
|
||||
AutoRunHistoryResponse,
|
||||
AssistantMessageResultState,
|
||||
AssistantConversationItem,
|
||||
ConnectionState,
|
||||
@@ -247,5 +250,36 @@ export const apiClient = {
|
||||
|
||||
async loadAssistantSession(sessionId: string): Promise<{ ok: boolean; session: { items: AssistantConversationItem[] } }> {
|
||||
return request(`/assistant/session/${sessionId}`);
|
||||
},
|
||||
|
||||
async loadAutoRunsHistory(input?: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
target?: string;
|
||||
mode?: string;
|
||||
use_mock?: "any" | "true" | "false";
|
||||
prompt_contains?: string;
|
||||
limit?: number;
|
||||
scan_limit?: number;
|
||||
}): Promise<AutoRunHistoryResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (input?.from) params.set("from", input.from);
|
||||
if (input?.to) params.set("to", input.to);
|
||||
if (input?.target) params.set("target", input.target);
|
||||
if (input?.mode) params.set("mode", input.mode);
|
||||
if (input?.use_mock) params.set("use_mock", input.use_mock);
|
||||
if (input?.prompt_contains) params.set("prompt_contains", input.prompt_contains);
|
||||
if (typeof input?.limit === "number") params.set("limit", String(input.limit));
|
||||
if (typeof input?.scan_limit === "number") params.set("scan_limit", String(input.scan_limit));
|
||||
const query = params.toString();
|
||||
return request(`/autoruns/history${query ? `?${query}` : ""}`);
|
||||
},
|
||||
|
||||
async loadAutoRunDetail(runId: string): Promise<AutoRunDetailResponse> {
|
||||
return request(`/autoruns/history/${encodeURIComponent(runId)}`);
|
||||
},
|
||||
|
||||
async loadAutoRunCaseDialog(runId: string, caseId: string): Promise<AutoRunDialogResponse> {
|
||||
return request(`/autoruns/history/${encodeURIComponent(runId)}/case/${encodeURIComponent(caseId)}/dialog`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,664 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { apiClient } from "../api/client";
|
||||
import type {
|
||||
AutoRunCaseSummary,
|
||||
AutoRunDetailResponse,
|
||||
AutoRunDialogResponse,
|
||||
AutoRunDomainCoverage,
|
||||
AutoRunHistoryResponse,
|
||||
AutoRunSummary,
|
||||
ConnectionState,
|
||||
PromptState
|
||||
} from "../state/types";
|
||||
import { JsonView } from "./JsonView";
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
|
||||
interface AutoRunsHistoryPanelProps {
|
||||
connection: ConnectionState;
|
||||
prompts: PromptState;
|
||||
assistantPromptVersion: string;
|
||||
decompositionPromptVersion: string;
|
||||
onLog?: (message: string) => void;
|
||||
}
|
||||
|
||||
type UseMockFilter = "any" | "true" | "false";
|
||||
|
||||
interface AutoRunsFilters {
|
||||
fromLocal: string;
|
||||
toLocal: string;
|
||||
target: string;
|
||||
mode: string;
|
||||
useMock: UseMockFilter;
|
||||
promptContains: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
const DEFAULT_FILTERS: AutoRunsFilters = {
|
||||
fromLocal: "",
|
||||
toLocal: "",
|
||||
target: "all",
|
||||
mode: "all",
|
||||
useMock: "any",
|
||||
promptContains: "",
|
||||
limit: 120
|
||||
};
|
||||
|
||||
function dateToInputValue(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hour = String(date.getHours()).padStart(2, "0");
|
||||
const minute = String(date.getMinutes()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}T${hour}:${minute}`;
|
||||
}
|
||||
|
||||
function defaultFromDateValue(): string {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 14);
|
||||
return dateToInputValue(date);
|
||||
}
|
||||
|
||||
function localInputToIso(value: string): string | undefined {
|
||||
if (!value.trim()) return undefined;
|
||||
const parsed = Date.parse(value);
|
||||
if (!Number.isFinite(parsed)) return undefined;
|
||||
return new Date(parsed).toISOString();
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string): string {
|
||||
const parsed = Date.parse(iso);
|
||||
if (!Number.isFinite(parsed)) return iso;
|
||||
return new Date(parsed).toLocaleString("ru-RU");
|
||||
}
|
||||
|
||||
function toPercent(closed: number, total: number): number {
|
||||
if (total <= 0) return 0;
|
||||
return Math.max(0, Math.min(100, Number(((closed / total) * 100).toFixed(1))));
|
||||
}
|
||||
|
||||
function formatScore(value: number | null): string {
|
||||
if (typeof value !== "number") return "n/a";
|
||||
return `${value.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function formatShortTarget(value: string): string {
|
||||
if (value === "assistant_stage1") return "assistant/s1";
|
||||
if (value === "assistant_stage2") return "assistant/s2";
|
||||
if (value === "assistant_p0") return "assistant/p0";
|
||||
return value;
|
||||
}
|
||||
|
||||
function trendLabel(value: "up" | "down" | "flat"): string {
|
||||
if (value === "up") return "Рост";
|
||||
if (value === "down") return "Регресс";
|
||||
return "Без изменений";
|
||||
}
|
||||
|
||||
function getSelectedCase(cases: AutoRunCaseSummary[], caseId: string): AutoRunCaseSummary | null {
|
||||
return cases.find((item) => item.case_id === caseId) ?? null;
|
||||
}
|
||||
|
||||
function renderCoverageRows(items: AutoRunDomainCoverage[]) {
|
||||
if (items.length === 0) {
|
||||
return <p className="muted">Покрытие доменов пока не сформировано.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="autoruns-coverage-list">
|
||||
{items.map((item) => {
|
||||
const percent = toPercent(item.closed_cases, item.total_cases);
|
||||
return (
|
||||
<div key={item.domain} className="autoruns-coverage-item">
|
||||
<div className="autoruns-coverage-head">
|
||||
<strong>{item.domain}</strong>
|
||||
<span>
|
||||
{item.closed_cases}/{item.total_cases} ({percent}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="autoruns-coverage-bar">
|
||||
<div style={{ width: `${percent}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AutoRunsHistoryPanel({
|
||||
connection,
|
||||
prompts,
|
||||
assistantPromptVersion,
|
||||
decompositionPromptVersion,
|
||||
onLog
|
||||
}: AutoRunsHistoryPanelProps) {
|
||||
const [filters, setFilters] = useState<AutoRunsFilters>({
|
||||
...DEFAULT_FILTERS,
|
||||
fromLocal: defaultFromDateValue()
|
||||
});
|
||||
const [history, setHistory] = useState<AutoRunHistoryResponse | null>(null);
|
||||
const [runDetail, setRunDetail] = useState<AutoRunDetailResponse | null>(null);
|
||||
const [dialog, setDialog] = useState<AutoRunDialogResponse | null>(null);
|
||||
const [selectedRunId, setSelectedRunId] = useState("");
|
||||
const [selectedCaseId, setSelectedCaseId] = useState("");
|
||||
const [historyBusy, setHistoryBusy] = useState(false);
|
||||
const [detailBusy, setDetailBusy] = useState(false);
|
||||
const [dialogBusy, setDialogBusy] = useState(false);
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [showAssistantMode, setShowAssistantMode] = useState(true);
|
||||
const [showDecompositionMode, setShowDecompositionMode] = useState(true);
|
||||
const [showProgressMode, setShowProgressMode] = useState(true);
|
||||
|
||||
const activeRunSummary: AutoRunSummary | null =
|
||||
history?.items.find((item) => item.run_id === selectedRunId) ?? null;
|
||||
const activeCase = runDetail ? getSelectedCase(runDetail.cases, selectedCaseId) : null;
|
||||
|
||||
const log = useCallback(
|
||||
(message: string) => {
|
||||
onLog?.(`[autoruns] ${message}`);
|
||||
},
|
||||
[onLog]
|
||||
);
|
||||
|
||||
const loadCaseDialog = useCallback(
|
||||
async (runId: string, caseId: string) => {
|
||||
setDialogBusy(true);
|
||||
try {
|
||||
const payload = await apiClient.loadAutoRunCaseDialog(runId, caseId);
|
||||
setDialog(payload);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setErrorText(`Диалог кейса: ${message}`);
|
||||
log(`Dialog load error for ${runId}/${caseId}: ${message}`);
|
||||
setDialog(null);
|
||||
} finally {
|
||||
setDialogBusy(false);
|
||||
}
|
||||
},
|
||||
[log]
|
||||
);
|
||||
|
||||
const loadRunDetail = useCallback(
|
||||
async (runId: string, preferredCaseId?: string) => {
|
||||
setDetailBusy(true);
|
||||
try {
|
||||
const payload = await apiClient.loadAutoRunDetail(runId);
|
||||
setRunDetail(payload);
|
||||
const nextCaseId =
|
||||
(preferredCaseId && payload.cases.some((item) => item.case_id === preferredCaseId) ? preferredCaseId : "") ||
|
||||
payload.cases[0]?.case_id ||
|
||||
"";
|
||||
setSelectedCaseId(nextCaseId);
|
||||
if (nextCaseId) {
|
||||
await loadCaseDialog(runId, nextCaseId);
|
||||
} else {
|
||||
setDialog(null);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setErrorText(`Детализация прогона: ${message}`);
|
||||
log(`Run detail load error for ${runId}: ${message}`);
|
||||
setRunDetail(null);
|
||||
setDialog(null);
|
||||
} finally {
|
||||
setDetailBusy(false);
|
||||
}
|
||||
},
|
||||
[loadCaseDialog, log]
|
||||
);
|
||||
|
||||
const loadHistory = useCallback(
|
||||
async (options?: { keepSelection?: boolean; preferredRunId?: string; preferredCaseId?: string }) => {
|
||||
setHistoryBusy(true);
|
||||
setErrorText("");
|
||||
try {
|
||||
const payload = await apiClient.loadAutoRunsHistory({
|
||||
from: localInputToIso(filters.fromLocal),
|
||||
to: localInputToIso(filters.toLocal),
|
||||
target: filters.target,
|
||||
mode: filters.mode,
|
||||
use_mock: filters.useMock,
|
||||
prompt_contains: filters.promptContains.trim() || undefined,
|
||||
limit: filters.limit
|
||||
});
|
||||
setHistory(payload);
|
||||
const hasRuns = payload.items.length > 0;
|
||||
if (!hasRuns) {
|
||||
setSelectedRunId("");
|
||||
setSelectedCaseId("");
|
||||
setRunDetail(null);
|
||||
setDialog(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const keepSelection = options?.keepSelection ?? true;
|
||||
const preferredRunId = options?.preferredRunId ?? "";
|
||||
const preferredCaseId = options?.preferredCaseId ?? "";
|
||||
const nextRunId =
|
||||
keepSelection && preferredRunId && payload.items.some((item) => item.run_id === preferredRunId)
|
||||
? preferredRunId
|
||||
: payload.items[0].run_id;
|
||||
setSelectedRunId(nextRunId);
|
||||
await loadRunDetail(nextRunId, keepSelection ? preferredCaseId : undefined);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setErrorText(`История прогонов: ${message}`);
|
||||
log(`History load error: ${message}`);
|
||||
} finally {
|
||||
setHistoryBusy(false);
|
||||
}
|
||||
},
|
||||
[filters.fromLocal, filters.limit, filters.mode, filters.promptContains, filters.target, filters.toLocal, filters.useMock, loadRunDetail, log]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void loadHistory({ keepSelection: false });
|
||||
}, [loadHistory]);
|
||||
|
||||
const dynamicColumns = useMemo(() => {
|
||||
const columns = ["minmax(290px, 340px)", "minmax(300px, 360px)", "minmax(420px, 1fr)"];
|
||||
if (showAssistantMode) columns.push("minmax(280px, 320px)");
|
||||
if (showDecompositionMode) columns.push("minmax(280px, 320px)");
|
||||
if (showProgressMode) columns.push("minmax(280px, 320px)");
|
||||
return columns.join(" ");
|
||||
}, [showAssistantMode, showDecompositionMode, showProgressMode]);
|
||||
|
||||
return (
|
||||
<PanelFrame
|
||||
title="История автопрогонов"
|
||||
subtitle="Центральный экран диагностики: фильтры, список прогонов, диалог по кейсу, режимы ассистента/декомпозиции и тренд качества."
|
||||
actions={
|
||||
<div className="assistant-panel-actions">
|
||||
<button type="button" className={showAssistantMode ? "tab active" : "tab"} onClick={() => setShowAssistantMode((prev) => !prev)}>
|
||||
Режим ассистента
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={showDecompositionMode ? "tab active" : "tab"}
|
||||
onClick={() => setShowDecompositionMode((prev) => !prev)}
|
||||
>
|
||||
Режим декомпозиции
|
||||
</button>
|
||||
<button type="button" className={showProgressMode ? "tab active" : "tab"} onClick={() => setShowProgressMode((prev) => !prev)}>
|
||||
Прогресс/регресс
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="autoruns-columns" style={{ gridTemplateColumns: dynamicColumns }}>
|
||||
<section className="autoruns-col">
|
||||
<h3>Настройки выборки</h3>
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Дата с
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={filters.fromLocal}
|
||||
onChange={(event) => setFilters((prev) => ({ ...prev, fromLocal: event.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Дата по
|
||||
<input type="datetime-local" value={filters.toLocal} onChange={(event) => setFilters((prev) => ({ ...prev, toLocal: event.target.value }))} />
|
||||
</label>
|
||||
<label>
|
||||
Целевой контур
|
||||
<select value={filters.target} onChange={(event) => setFilters((prev) => ({ ...prev, target: event.target.value }))}>
|
||||
<option value="all">all</option>
|
||||
{(history?.available.targets ?? []).map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Режим
|
||||
<select value={filters.mode} onChange={(event) => setFilters((prev) => ({ ...prev, mode: event.target.value }))}>
|
||||
<option value="all">all</option>
|
||||
{(history?.available.modes ?? []).map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
use_mock
|
||||
<select value={filters.useMock} onChange={(event) => setFilters((prev) => ({ ...prev, useMock: event.target.value as UseMockFilter }))}>
|
||||
<option value="any">any</option>
|
||||
<option value="true">true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Лимит
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={500}
|
||||
value={filters.limit}
|
||||
onChange={(event) => setFilters((prev) => ({ ...prev, limit: Number(event.target.value || 120) }))}
|
||||
/>
|
||||
</label>
|
||||
<label className="full-width">
|
||||
Версия промпта содержит
|
||||
<input
|
||||
value={filters.promptContains}
|
||||
onChange={(event) => setFilters((prev) => ({ ...prev, promptContains: event.target.value }))}
|
||||
placeholder="normalizer_v2_0_2 / address_query_runtime_v1"
|
||||
list="autoruns-prompt-versions"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<datalist id="autoruns-prompt-versions">
|
||||
{(history?.available.prompt_versions ?? []).map((item) => (
|
||||
<option key={item} value={item} />
|
||||
))}
|
||||
</datalist>
|
||||
<div className="button-row">
|
||||
<button type="button" disabled={historyBusy} onClick={() => void loadHistory({ keepSelection: false })}>
|
||||
{historyBusy ? "Обновляю..." : "Применить"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tab"
|
||||
onClick={() => {
|
||||
setFilters({
|
||||
...DEFAULT_FILTERS,
|
||||
fromLocal: defaultFromDateValue()
|
||||
});
|
||||
setErrorText("");
|
||||
}}
|
||||
>
|
||||
Сбросить фильтры
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h4>Контур генерации</h4>
|
||||
<div className="autoruns-meta-list">
|
||||
<div>
|
||||
<span>Провайдер:</span>
|
||||
<strong>{connection.llmProvider}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Модель:</span>
|
||||
<strong>{connection.model || "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Prompt assistant:</span>
|
||||
<strong>{assistantPromptVersion}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Prompt decomposition:</span>
|
||||
<strong>{decompositionPromptVersion}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details className="autoruns-prompt-details">
|
||||
<summary>Дублирование главного промпта (read-only)</summary>
|
||||
<label>
|
||||
System
|
||||
<textarea readOnly value={prompts.systemPrompt} />
|
||||
</label>
|
||||
<label>
|
||||
Developer
|
||||
<textarea readOnly value={prompts.developerPrompt} />
|
||||
</label>
|
||||
<label>
|
||||
Domain
|
||||
<textarea readOnly value={prompts.domainPrompt} />
|
||||
</label>
|
||||
<label>
|
||||
Schema notes
|
||||
<textarea readOnly value={prompts.schemaNotes} />
|
||||
</label>
|
||||
<label>
|
||||
Few-shot
|
||||
<textarea readOnly value={prompts.fewShotExamples} />
|
||||
</label>
|
||||
</details>
|
||||
|
||||
{errorText ? <p className="error-text">{errorText}</p> : null}
|
||||
</section>
|
||||
|
||||
<section className="autoruns-col">
|
||||
<h3>Выдача прогонов</h3>
|
||||
<div className="autoruns-stats-grid">
|
||||
<div>
|
||||
<span>Всего</span>
|
||||
<strong>{history?.stats.runs_total ?? 0}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Средний score</span>
|
||||
<strong>{formatScore(history?.stats.avg_score_index ?? null)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Тренд</span>
|
||||
<strong>{history ? trendLabel(history.stats.trend) : "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Блокеры</span>
|
||||
<strong>{history?.stats.blocking_runs ?? 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="autoruns-run-list">
|
||||
{(history?.items ?? []).map((run) => (
|
||||
<button
|
||||
key={run.run_id}
|
||||
type="button"
|
||||
className={selectedRunId === run.run_id ? "autoruns-run-item selected" : "autoruns-run-item"}
|
||||
onClick={() => {
|
||||
setSelectedRunId(run.run_id);
|
||||
void loadRunDetail(run.run_id);
|
||||
}}
|
||||
>
|
||||
<div className="autoruns-run-head">
|
||||
<strong>{formatDateTime(run.run_timestamp)}</strong>
|
||||
<span>{formatShortTarget(run.eval_target)}</span>
|
||||
</div>
|
||||
<div className="autoruns-run-meta">{run.run_id}</div>
|
||||
<div className="autoruns-run-meta">
|
||||
mode={run.mode ?? "n/a"} | mock={String(run.use_mock)}
|
||||
</div>
|
||||
{run.llm_provider || run.model ? (
|
||||
<div className="autoruns-run-meta">
|
||||
llm={run.llm_provider ?? "n/a"} | model={run.model ?? "n/a"}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="autoruns-run-meta">prompt={run.prompt_version ?? "n/a"}</div>
|
||||
<div className="autoruns-run-foot">
|
||||
<span>score: {formatScore(run.score_index)}</span>
|
||||
<span>
|
||||
closed/open: {run.closed_cases}/{run.open_cases}
|
||||
</span>
|
||||
</div>
|
||||
<div className="autoruns-run-foot">
|
||||
<span>blocking: {run.blocking_failures}</span>
|
||||
<span>quality: {run.quality_failures}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{(history?.items.length ?? 0) === 0 ? <p className="muted">За выбранный диапазон прогонов нет.</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="autoruns-col">
|
||||
<h3>Диалог прогона</h3>
|
||||
<div className="autoruns-dialog-toolbar">
|
||||
<label>
|
||||
Прогон
|
||||
<select
|
||||
value={selectedRunId}
|
||||
onChange={(event) => {
|
||||
const nextRunId = event.target.value;
|
||||
setSelectedRunId(nextRunId);
|
||||
void loadRunDetail(nextRunId);
|
||||
}}
|
||||
>
|
||||
{(history?.items ?? []).map((item) => (
|
||||
<option key={item.run_id} value={item.run_id}>
|
||||
{formatDateTime(item.run_timestamp)} | {item.run_id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Кейc
|
||||
<select
|
||||
value={selectedCaseId}
|
||||
onChange={(event) => {
|
||||
const nextCaseId = event.target.value;
|
||||
setSelectedCaseId(nextCaseId);
|
||||
if (selectedRunId && nextCaseId) {
|
||||
void loadCaseDialog(selectedRunId, nextCaseId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(runDetail?.cases ?? []).map((item) => (
|
||||
<option key={item.case_id} value={item.case_id}>
|
||||
{item.case_id} | {item.status}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="autoruns-case-list">
|
||||
{(runDetail?.cases ?? []).map((item) => (
|
||||
<button
|
||||
key={item.case_id}
|
||||
type="button"
|
||||
className={selectedCaseId === item.case_id ? "autoruns-case-item selected" : "autoruns-case-item"}
|
||||
onClick={() => {
|
||||
setSelectedCaseId(item.case_id);
|
||||
if (selectedRunId) {
|
||||
void loadCaseDialog(selectedRunId, item.case_id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{item.case_id}</span>
|
||||
<span>{item.status}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="autoruns-dialog-view">
|
||||
{dialogBusy || detailBusy ? <p className="muted">Загружаю диалог...</p> : null}
|
||||
{!dialogBusy && !detailBusy && (dialog?.messages.length ?? 0) === 0 ? <p className="muted">Диалог для этого кейса не найден.</p> : null}
|
||||
{(dialog?.messages ?? []).map((item, index) => {
|
||||
const role = item.role === "assistant" ? "assistant" : "user";
|
||||
return (
|
||||
<article key={`${role}-${index}`} className={`autoruns-msg ${role}`}>
|
||||
<header>
|
||||
<strong>{role === "assistant" ? "Система" : "Модель/вопрос"}</strong>
|
||||
<span>{item.created_at ? formatDateTime(item.created_at) : "n/a"}</span>
|
||||
</header>
|
||||
<p>{item.text}</p>
|
||||
{(item.trace_id || item.reply_type) && (
|
||||
<footer>
|
||||
{item.trace_id ? <span>trace={item.trace_id}</span> : null}
|
||||
{item.reply_type ? <span>reply_type={item.reply_type}</span> : null}
|
||||
</footer>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showAssistantMode ? (
|
||||
<section className="autoruns-col">
|
||||
<h3>Режим ассистента</h3>
|
||||
<div className="autoruns-meta-list">
|
||||
<div>
|
||||
<span>source:</span>
|
||||
<strong>{dialog?.source ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>session:</span>
|
||||
<strong>{dialog?.session_id ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>run target:</span>
|
||||
<strong>{activeRunSummary?.eval_target ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>run score:</span>
|
||||
<strong>{formatScore(activeRunSummary?.score_index ?? null)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<h4>Assistant mode payload</h4>
|
||||
<JsonView value={dialog?.assistant_mode ?? { note: "assistant_mode unavailable" }} />
|
||||
<h4 style={{ marginTop: 12 }}>Case checks</h4>
|
||||
<JsonView value={activeCase?.checks ?? { note: "checks unavailable" }} />
|
||||
<h4 style={{ marginTop: 12 }}>Metric subscores</h4>
|
||||
<JsonView value={activeCase?.metric_subscores ?? { note: "metric_subscores unavailable" }} />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{showDecompositionMode ? (
|
||||
<section className="autoruns-col">
|
||||
<h3>Режим декомпозиции</h3>
|
||||
<div className="autoruns-meta-list">
|
||||
<div>
|
||||
<span>Case:</span>
|
||||
<strong>{activeCase?.case_id ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Domain:</span>
|
||||
<strong>{activeCase?.domain ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Query class:</span>
|
||||
<strong>{activeCase?.query_class ?? "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Trace:</span>
|
||||
<strong>{activeCase?.trace_id ?? "n/a"}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<h4>Шаги декомпозиции</h4>
|
||||
{(dialog?.decomposition.length ?? 0) > 0 ? (
|
||||
<ol className="autoruns-decomposition-list">
|
||||
{(dialog?.decomposition ?? []).map((item, index) => (
|
||||
<li key={`${index}-${item.slice(0, 24)}`}>{item}</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<p className="muted">В логах кейса нет явной декомпозиции.</p>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{showProgressMode ? (
|
||||
<section className="autoruns-col">
|
||||
<h3>Прогресс / регресс</h3>
|
||||
<div className="autoruns-stats-grid">
|
||||
<div>
|
||||
<span>Latest score</span>
|
||||
<strong>{formatScore(history?.stats.latest_score_index ?? null)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Previous</span>
|
||||
<strong>{formatScore(history?.stats.previous_score_index ?? null)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Trend</span>
|
||||
<strong>{history ? trendLabel(history.stats.trend) : "n/a"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Quality gaps</span>
|
||||
<strong>{history?.stats.quality_gap_runs ?? 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<h4>Покрытие доменов (история)</h4>
|
||||
{renderCoverageRows(history?.stats.domain_coverage ?? [])}
|
||||
<h4 style={{ marginTop: 14 }}>Покрытие доменов (выбранный прогон)</h4>
|
||||
{renderCoverageRows(runDetail?.coverage.domain_coverage ?? [])}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
@@ -66,7 +66,119 @@ export interface RuntimeRun {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type UiMode = "assistant" | "decomposition";
|
||||
export type UiMode = "assistant" | "decomposition" | "autoruns";
|
||||
|
||||
export type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
|
||||
export type AutoRunTrend = "up" | "down" | "flat";
|
||||
|
||||
export interface AutoRunDomainCoverage {
|
||||
domain: string;
|
||||
total_cases: number;
|
||||
closed_cases: number;
|
||||
}
|
||||
|
||||
export interface AutoRunSummary {
|
||||
run_id: string;
|
||||
eval_target: AutoRunTarget;
|
||||
run_timestamp: string;
|
||||
mode: string | null;
|
||||
llm_provider: string | null;
|
||||
model: string | null;
|
||||
use_mock: boolean | null;
|
||||
prompt_version: string | null;
|
||||
schema_version: string | null;
|
||||
suite_id: string | null;
|
||||
cases_total: number;
|
||||
requests_total: number | null;
|
||||
report_path: string;
|
||||
score_index: number | null;
|
||||
blocking_failures: number;
|
||||
quality_failures: number;
|
||||
closed_cases: number;
|
||||
open_cases: number;
|
||||
domain_coverage: AutoRunDomainCoverage[];
|
||||
}
|
||||
|
||||
export interface AutoRunCaseSummary {
|
||||
case_id: string;
|
||||
domain: string | null;
|
||||
query_class: string | null;
|
||||
status: "closed" | "open" | "unknown";
|
||||
score_index: number | null;
|
||||
trace_id: string | null;
|
||||
reply_type: string | null;
|
||||
session_id: string;
|
||||
dialog_available: boolean;
|
||||
checks: Record<string, unknown> | null;
|
||||
metric_subscores: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface AutoRunCoverage {
|
||||
closed_cases: number;
|
||||
open_cases: number;
|
||||
domain_coverage: AutoRunDomainCoverage[];
|
||||
}
|
||||
|
||||
export interface AutoRunHistoryStats {
|
||||
runs_total: number;
|
||||
by_target: Record<string, number>;
|
||||
blocking_runs: number;
|
||||
quality_gap_runs: number;
|
||||
avg_score_index: number | null;
|
||||
latest_score_index: number | null;
|
||||
previous_score_index: number | null;
|
||||
trend: AutoRunTrend;
|
||||
domain_coverage: AutoRunDomainCoverage[];
|
||||
}
|
||||
|
||||
export interface AutoRunHistoryResponse {
|
||||
ok: boolean;
|
||||
generated_at: string;
|
||||
filters_applied: {
|
||||
from: string | null;
|
||||
to: string | null;
|
||||
target: AutoRunTarget | "all";
|
||||
use_mock: boolean | null;
|
||||
prompt_contains: string;
|
||||
mode: string;
|
||||
limit: number;
|
||||
scan_limit: number;
|
||||
};
|
||||
available: {
|
||||
targets: AutoRunTarget[];
|
||||
modes: string[];
|
||||
prompt_versions: string[];
|
||||
};
|
||||
items: AutoRunSummary[];
|
||||
stats: AutoRunHistoryStats;
|
||||
}
|
||||
|
||||
export interface AutoRunDetailResponse {
|
||||
ok: boolean;
|
||||
run: AutoRunSummary;
|
||||
coverage: AutoRunCoverage;
|
||||
cases: AutoRunCaseSummary[];
|
||||
report: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AutoRunDialogMessage {
|
||||
role: string;
|
||||
text: string;
|
||||
created_at: string | null;
|
||||
trace_id: string | null;
|
||||
reply_type: string | null;
|
||||
}
|
||||
|
||||
export interface AutoRunDialogResponse {
|
||||
ok: boolean;
|
||||
run_id: string;
|
||||
case_id: string;
|
||||
source: "assistant_session" | "report_fallback" | "none";
|
||||
session_id: string;
|
||||
messages: AutoRunDialogMessage[];
|
||||
decomposition: string[];
|
||||
assistant_mode: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type AssistantFallbackType = "none" | "out_of_scope" | "clarification" | "partial" | "unknown";
|
||||
export type AssistantReplyType =
|
||||
|
||||
@@ -454,10 +454,263 @@ button:disabled {
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.autoruns-columns {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.autoruns-col {
|
||||
border: 1px solid rgba(157, 255, 190, 0.2);
|
||||
border-radius: 14px;
|
||||
background: rgba(8, 13, 10, 0.72);
|
||||
padding: 12px;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.autoruns-col h3 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.autoruns-col h4 {
|
||||
margin: 12px 0 8px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.autoruns-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-meta-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-meta-list > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(157, 255, 190, 0.14);
|
||||
border-radius: 10px;
|
||||
background: rgba(10, 18, 13, 0.65);
|
||||
padding: 8px 9px;
|
||||
font-size: 0.79rem;
|
||||
}
|
||||
|
||||
.autoruns-meta-list span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.autoruns-prompt-details summary {
|
||||
cursor: pointer;
|
||||
color: var(--lime-main);
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.autoruns-prompt-details textarea {
|
||||
min-height: 68px;
|
||||
}
|
||||
|
||||
.autoruns-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.autoruns-stats-grid > div {
|
||||
border: 1px solid rgba(157, 255, 190, 0.2);
|
||||
border-radius: 10px;
|
||||
background: rgba(10, 18, 13, 0.7);
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.autoruns-stats-grid span {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.autoruns-stats-grid strong {
|
||||
color: var(--lime-main);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.autoruns-run-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: 760px;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.autoruns-run-item {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(157, 255, 190, 0.23);
|
||||
background: rgba(11, 18, 14, 0.75);
|
||||
color: var(--text-main);
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.autoruns-run-item.selected {
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.autoruns-run-head,
|
||||
.autoruns-run-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.autoruns-run-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.autoruns-dialog-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-case-list {
|
||||
margin-top: 8px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
max-height: 170px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.autoruns-case-item {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(157, 255, 190, 0.22);
|
||||
background: rgba(9, 14, 11, 0.72);
|
||||
color: var(--text-main);
|
||||
padding: 7px 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.autoruns-case-item.selected {
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.autoruns-dialog-view {
|
||||
margin-top: 10px;
|
||||
border: 1px solid rgba(157, 255, 190, 0.2);
|
||||
border-radius: 12px;
|
||||
background: rgba(5, 8, 6, 0.7);
|
||||
padding: 10px;
|
||||
max-height: 570px;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-msg {
|
||||
border: 1px solid rgba(157, 255, 190, 0.22);
|
||||
border-radius: 12px;
|
||||
background: rgba(11, 18, 14, 0.8);
|
||||
padding: 8px 10px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.autoruns-msg header,
|
||||
.autoruns-msg footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.autoruns-msg p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.35;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.autoruns-msg.assistant {
|
||||
margin-right: 12%;
|
||||
}
|
||||
|
||||
.autoruns-msg.user {
|
||||
margin-left: 12%;
|
||||
border-color: rgba(95, 179, 255, 0.35);
|
||||
background: rgba(10, 18, 24, 0.75);
|
||||
}
|
||||
|
||||
.autoruns-decomposition-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.autoruns-coverage-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.autoruns-coverage-item {
|
||||
border: 1px solid rgba(157, 255, 190, 0.2);
|
||||
border-radius: 10px;
|
||||
background: rgba(11, 18, 14, 0.68);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.autoruns-coverage-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 0.76rem;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.autoruns-coverage-head span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.autoruns-coverage-bar {
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(157, 255, 190, 0.14);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.autoruns-coverage-bar > div {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #6ee0ff, #8fffad);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.autoruns-columns {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
@@ -469,6 +722,12 @@ button:disabled {
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.autoruns-form-grid,
|
||||
.autoruns-dialog-toolbar,
|
||||
.autoruns-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/components/assistantpanel.tsx","./src/components/connectionpanel.tsx","./src/components/historypanel.tsx","./src/components/jsonview.tsx","./src/components/metricspanel.tsx","./src/components/outputpanel.tsx","./src/components/panelframe.tsx","./src/components/promptpanel.tsx","./src/components/querypanel.tsx","./src/components/runtimepanel.tsx","./src/state/defaults.ts","./src/state/types.ts","./src/utils/conversationexport.ts"],"version":"5.9.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/components/assistantpanel.tsx","./src/components/autorunshistorypanel.tsx","./src/components/connectionpanel.tsx","./src/components/historypanel.tsx","./src/components/jsonview.tsx","./src/components/metricspanel.tsx","./src/components/outputpanel.tsx","./src/components/panelframe.tsx","./src/components/promptpanel.tsx","./src/components/querypanel.tsx","./src/components/runtimepanel.tsx","./src/state/defaults.ts","./src/state/types.ts","./src/utils/conversationexport.ts"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user