Initial import NDC_1C
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { AssistantConversationItem } from "../state/types";
|
||||
import { JsonView } from "./JsonView";
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
|
||||
interface AssistantPanelProps {
|
||||
sessionId: string;
|
||||
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;
|
||||
onClear: () => void;
|
||||
busy: boolean;
|
||||
statusText: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
function roleLabel(role: AssistantConversationItem["role"]): string {
|
||||
return role === "assistant" ? "Ассистент" : "Вы";
|
||||
}
|
||||
|
||||
function shortTime(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return iso;
|
||||
}
|
||||
return date.toLocaleTimeString("ru-RU");
|
||||
}
|
||||
|
||||
export function AssistantPanel({
|
||||
sessionId,
|
||||
conversation,
|
||||
inputValue,
|
||||
onInputChange,
|
||||
periodHint,
|
||||
onPeriodHintChange,
|
||||
businessContext,
|
||||
onBusinessContextChange,
|
||||
useMock,
|
||||
onUseMockChange,
|
||||
onSend,
|
||||
onClear,
|
||||
busy,
|
||||
statusText,
|
||||
errorMessage
|
||||
}: AssistantPanelProps) {
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
listRef.current.scrollTop = listRef.current.scrollHeight;
|
||||
}
|
||||
}, [conversation, statusText]);
|
||||
|
||||
return (
|
||||
<PanelFrame
|
||||
title="Режим ассистента"
|
||||
subtitle="Диалоговый слой поверх normalizer, маршрутизации и factual retrieval."
|
||||
actions={<span className="status-chip">{sessionId ? `session: ${sessionId}` : "новая сессия"}</span>}
|
||||
>
|
||||
<div ref={listRef} className="assistant-chat-list">
|
||||
{conversation.length === 0 ? (
|
||||
<div className="assistant-empty muted">Диалог пуст. Отправьте первый вопрос, чтобы запустить контур ассистента.</div>
|
||||
) : null}
|
||||
{conversation.map((item) => (
|
||||
<article key={item.message_id} className={`assistant-msg ${item.role}`}>
|
||||
<header className="assistant-msg-head">
|
||||
<strong>{roleLabel(item.role)}</strong>
|
||||
<span>{shortTime(item.created_at)}</span>
|
||||
</header>
|
||||
<div className="assistant-msg-body">{item.text}</div>
|
||||
{item.role === "assistant" && item.debug ? (
|
||||
<details className="assistant-debug">
|
||||
<summary>Показать технический разбор</summary>
|
||||
<JsonView value={item.debug} />
|
||||
</details>
|
||||
) : null}
|
||||
</article>
|
||||
))}
|
||||
</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
|
||||
value={inputValue}
|
||||
onChange={(event) => onInputChange(event.target.value)}
|
||||
rows={4}
|
||||
placeholder="Введите вопрос к данным компании..."
|
||||
/>
|
||||
</label>
|
||||
<div className="button-row">
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={useMock} onChange={(event) => onUseMockChange(event.target.checked)} />
|
||||
Mock-режим
|
||||
</label>
|
||||
<button type="button" onClick={() => onSend()} disabled={busy || !inputValue.trim()}>
|
||||
{busy ? "Выполняю..." : "Отправить"}
|
||||
</button>
|
||||
<button type="button" onClick={() => onClear()} disabled={busy && conversation.length === 0}>
|
||||
Сбросить сессию
|
||||
</button>
|
||||
</div>
|
||||
{statusText ? <p className="diff-summary">{statusText}</p> : null}
|
||||
{errorMessage ? <p className="error-text">{errorMessage}</p> : null}
|
||||
</div>
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
import type { ConnectionState } from "../state/types";
|
||||
|
||||
interface ConnectionPanelProps {
|
||||
value: ConnectionState;
|
||||
onChange: (next: ConnectionState) => void;
|
||||
onTestConnection: () => Promise<void> | void;
|
||||
onSaveLocalConfig: () => void;
|
||||
lastStatus: string;
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
export function ConnectionPanel({
|
||||
value,
|
||||
onChange,
|
||||
onTestConnection,
|
||||
onSaveLocalConfig,
|
||||
lastStatus,
|
||||
busy
|
||||
}: ConnectionPanelProps) {
|
||||
return (
|
||||
<PanelFrame
|
||||
title="Подключение OpenAI"
|
||||
subtitle="Ключ живет только в памяти сессии (не пишется в localStorage)."
|
||||
actions={<span className="status-chip">{lastStatus || "Статус: не проверено"}</span>}
|
||||
>
|
||||
<div className="grid-two">
|
||||
<label>
|
||||
OpenAI API Key
|
||||
<input
|
||||
type="password"
|
||||
value={value.apiKey}
|
||||
onChange={(event) => onChange({ ...value, apiKey: event.target.value })}
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Model ID
|
||||
<input value={value.model} onChange={(event) => onChange({ ...value, model: event.target.value })} />
|
||||
</label>
|
||||
<label>
|
||||
Base URL
|
||||
<input value={value.baseUrl} onChange={(event) => onChange({ ...value, baseUrl: event.target.value })} />
|
||||
</label>
|
||||
<label>
|
||||
Temperature
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={value.temperature}
|
||||
onChange={(event) => onChange({ ...value, temperature: Number(event.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Max output tokens
|
||||
<input
|
||||
type="number"
|
||||
value={value.maxOutputTokens}
|
||||
onChange={(event) => onChange({ ...value, maxOutputTokens: Number(event.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="button-row">
|
||||
<button type="button" onClick={() => onSaveLocalConfig()}>
|
||||
Сохранить локальную конфигурацию
|
||||
</button>
|
||||
<button type="button" onClick={() => onTestConnection()} disabled={busy}>
|
||||
{busy ? "Проверяем..." : "Проверить подключение"}
|
||||
</button>
|
||||
</div>
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
import type { HistoryItem } from "../state/types";
|
||||
|
||||
interface HistoryPanelProps {
|
||||
items: HistoryItem[];
|
||||
onRefresh: () => Promise<void> | void;
|
||||
onOpenTrace: (traceId: string) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export function HistoryPanel({ items, onRefresh, onOpenTrace }: HistoryPanelProps) {
|
||||
return (
|
||||
<PanelFrame
|
||||
title="История нормализаций"
|
||||
subtitle="Короткий вопрос, confidence, route hint и статус валидации."
|
||||
actions={
|
||||
<button type="button" onClick={() => onRefresh()}>
|
||||
Обновить
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="history-list">
|
||||
{items.length === 0 ? <p className="muted">История пока пустая.</p> : null}
|
||||
{items.map((item) => (
|
||||
<button key={item.trace_id} type="button" className="history-item" onClick={() => onOpenTrace(item.trace_id)}>
|
||||
<div className="history-row">
|
||||
<strong>{item.route_hint ?? "route: n/a"}</strong>
|
||||
<span>{item.validation_passed ? "schema: ok" : "schema: fail"}</span>
|
||||
</div>
|
||||
<p>{item.question_short}</p>
|
||||
<div className="history-row">
|
||||
<span>{item.model}</span>
|
||||
<span>{new Date(item.timestamp).toLocaleString("ru-RU")}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
interface JsonViewProps {
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export function JsonView({ value }: JsonViewProps) {
|
||||
return <pre className="json-view">{JSON.stringify(value ?? {}, null, 2)}</pre>;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
import type { NormalizeResultState } from "../state/types";
|
||||
|
||||
interface MetricsPanelProps {
|
||||
result: NormalizeResultState | null;
|
||||
}
|
||||
|
||||
function metric(value: string | number | boolean | null | undefined): string {
|
||||
if (value === null || value === undefined || value === "") return "—";
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function MetricsPanel({ result }: MetricsPanelProps) {
|
||||
return (
|
||||
<PanelFrame title="Runtime метрики" subtitle="trace_id, токены, latency и статус валидации.">
|
||||
<div className="metrics-grid">
|
||||
<div>
|
||||
<span>trace_id</span>
|
||||
<strong>{metric(result?.trace_id)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>request_started_at</span>
|
||||
<strong>{metric(result ? new Date(Date.now() - result.latency_ms).toISOString() : null)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>request_finished_at</span>
|
||||
<strong>{metric(result ? new Date().toISOString() : null)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>latency_ms</span>
|
||||
<strong>{metric(result?.latency_ms)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>input_tokens</span>
|
||||
<strong>{metric(result?.usage?.input_tokens)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>output_tokens</span>
|
||||
<strong>{metric(result?.usage?.output_tokens)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>total_tokens</span>
|
||||
<strong>{metric(result?.usage?.total_tokens)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>validation_status</span>
|
||||
<strong>{result?.validation?.passed ? "passed" : "failed"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>prompt_version</span>
|
||||
<strong>{metric(result?.prompt_version)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>schema_version</span>
|
||||
<strong>{metric(result?.schema_version)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
import { JsonView } from "./JsonView";
|
||||
import type { NormalizeResultState, TabKey } from "../state/types";
|
||||
|
||||
interface OutputPanelProps {
|
||||
tab: TabKey;
|
||||
onTabChange: (tab: TabKey) => void;
|
||||
result: NormalizeResultState | null;
|
||||
appLogs: string[];
|
||||
}
|
||||
|
||||
const TAB_LABELS: Record<TabKey, string> = {
|
||||
normalized: "Normalized JSON",
|
||||
fragments: "Fragment View",
|
||||
scope: "Scope View",
|
||||
flags: "Flags View",
|
||||
route: "Route Simulation",
|
||||
raw: "Raw model output",
|
||||
validation: "Validation",
|
||||
logs: "Logs"
|
||||
};
|
||||
|
||||
function asObject(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
export function OutputPanel({ tab, onTabChange, result, appLogs }: OutputPanelProps) {
|
||||
const tabs: TabKey[] = ["normalized", "fragments", "scope", "flags", "route", "raw", "validation", "logs"];
|
||||
const normalized = asObject(result?.normalized);
|
||||
const schemaVersion = String(normalized?.schema_version ?? "");
|
||||
const isV2 =
|
||||
schemaVersion === "normalized_query_v2" ||
|
||||
schemaVersion === "normalized_query_v2_0_1" ||
|
||||
schemaVersion === "normalized_query_v2_0_2";
|
||||
|
||||
const fragmentsView = isV2
|
||||
? {
|
||||
fragments: normalized?.fragments ?? [],
|
||||
discarded_fragments: normalized?.discarded_fragments ?? []
|
||||
}
|
||||
: { note: "Fragment View доступен для normalized_query_v2." };
|
||||
|
||||
const scopeView = isV2
|
||||
? {
|
||||
message_in_scope: normalized?.message_in_scope ?? null,
|
||||
scope_confidence: normalized?.scope_confidence ?? null,
|
||||
contains_multiple_tasks: normalized?.contains_multiple_tasks ?? null,
|
||||
global_notes: normalized?.global_notes ?? null
|
||||
}
|
||||
: { note: "Scope View доступен для normalized_query_v2." };
|
||||
|
||||
const flagsView = isV2
|
||||
? Array.isArray(normalized?.fragments)
|
||||
? (normalized?.fragments as Array<Record<string, unknown>>).map((fragment) => ({
|
||||
fragment_id: fragment.fragment_id ?? null,
|
||||
domain_relevance: fragment.domain_relevance ?? null,
|
||||
candidate_labels: fragment.candidate_labels ?? [],
|
||||
execution_readiness: fragment.execution_readiness ?? null,
|
||||
clarification_reason: fragment.clarification_reason ?? null,
|
||||
soft_assumption_used: fragment.soft_assumption_used ?? [],
|
||||
route_status: fragment.route_status ?? null,
|
||||
no_route_reason: fragment.no_route_reason ?? null,
|
||||
flags: fragment.flags ?? {}
|
||||
}))
|
||||
: []
|
||||
: { note: "Flags View доступен для normalized_query_v2." };
|
||||
|
||||
return (
|
||||
<PanelFrame title="Выходные данные" subtitle="Structured output и диагностические вкладки.">
|
||||
<div className="tab-row">
|
||||
{tabs.map((item) => (
|
||||
<button key={item} type="button" className={tab === item ? "tab active" : "tab"} onClick={() => onTabChange(item)}>
|
||||
{TAB_LABELS[item]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{tab === "normalized" ? <JsonView value={result?.normalized ?? { note: "Нет данных." }} /> : null}
|
||||
{tab === "fragments" ? <JsonView value={fragmentsView} /> : null}
|
||||
{tab === "scope" ? <JsonView value={scopeView} /> : null}
|
||||
{tab === "flags" ? <JsonView value={flagsView} /> : null}
|
||||
{tab === "route" ? <JsonView value={result?.route_hint_summary ?? { note: "Нет данных." }} /> : null}
|
||||
{tab === "raw" ? <JsonView value={result?.raw_model_output ?? { note: "Нет данных." }} /> : null}
|
||||
{tab === "validation" ? <JsonView value={result?.validation ?? { note: "Нет данных." }} /> : null}
|
||||
{tab === "logs" ? <JsonView value={appLogs} /> : null}
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
interface PanelFrameProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
export function PanelFrame({ title, subtitle, actions, children }: PropsWithChildren<PanelFrameProps>) {
|
||||
return (
|
||||
<section className="panel-frame">
|
||||
<header className="panel-header">
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{subtitle ? <p>{subtitle}</p> : null}
|
||||
</div>
|
||||
{actions ? <div className="panel-actions">{actions}</div> : null}
|
||||
</header>
|
||||
<div className="panel-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
import type { PromptState } from "../state/types";
|
||||
|
||||
interface PromptPresetListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
prompt_version: string;
|
||||
systemPrompt: string;
|
||||
developerPrompt: string;
|
||||
domainPrompt: string;
|
||||
schemaNotes?: string;
|
||||
fewShotExamples?: string;
|
||||
}
|
||||
|
||||
interface PromptPanelProps {
|
||||
value: PromptState;
|
||||
onChange: (next: PromptState) => void;
|
||||
presets: PromptPresetListItem[];
|
||||
selectedPresetId: string;
|
||||
onSelectPreset: (id: string) => void;
|
||||
onLoadPreset: () => void;
|
||||
onSavePreset: () => void;
|
||||
onResetDefaults: () => void;
|
||||
onDiffPrevious: () => void;
|
||||
presetName: string;
|
||||
onPresetNameChange: (name: string) => void;
|
||||
diffSummary: string;
|
||||
}
|
||||
|
||||
export function PromptPanel({
|
||||
value,
|
||||
onChange,
|
||||
presets,
|
||||
selectedPresetId,
|
||||
onSelectPreset,
|
||||
onLoadPreset,
|
||||
onSavePreset,
|
||||
onResetDefaults,
|
||||
onDiffPrevious,
|
||||
presetName,
|
||||
onPresetNameChange,
|
||||
diffSummary
|
||||
}: PromptPanelProps) {
|
||||
return (
|
||||
<PanelFrame title="Prompt Manager" subtitle="Системный, developer и domain уровни управляются отдельно.">
|
||||
<div className="grid-two">
|
||||
<label>
|
||||
Системный prompt
|
||||
<textarea
|
||||
value={value.systemPrompt}
|
||||
onChange={(event) => onChange({ ...value, systemPrompt: event.target.value })}
|
||||
rows={6}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Developer / Instruction prompt
|
||||
<textarea
|
||||
value={value.developerPrompt}
|
||||
onChange={(event) => onChange({ ...value, developerPrompt: event.target.value })}
|
||||
rows={6}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Domain prompt
|
||||
<textarea value={value.domainPrompt} onChange={(event) => onChange({ ...value, domainPrompt: event.target.value })} rows={6} />
|
||||
</label>
|
||||
<label>
|
||||
Schema notes
|
||||
<textarea value={value.schemaNotes} onChange={(event) => onChange({ ...value, schemaNotes: event.target.value })} rows={6} />
|
||||
</label>
|
||||
<label className="full-width">
|
||||
Few-shot examples
|
||||
<textarea
|
||||
value={value.fewShotExamples}
|
||||
onChange={(event) => onChange({ ...value, fewShotExamples: event.target.value })}
|
||||
rows={8}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="button-row">
|
||||
<select value={selectedPresetId} onChange={(event) => onSelectPreset(event.target.value)}>
|
||||
<option value="">Выберите preset...</option>
|
||||
{presets.map((preset) => (
|
||||
<option value={preset.id} key={preset.id}>
|
||||
{preset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" onClick={() => onLoadPreset()}>
|
||||
Загрузить preset
|
||||
</button>
|
||||
<input value={presetName} onChange={(event) => onPresetNameChange(event.target.value)} placeholder="Имя для сохранения" />
|
||||
<button type="button" onClick={() => onSavePreset()}>
|
||||
Сохранить preset
|
||||
</button>
|
||||
<button type="button" onClick={() => onDiffPrevious()}>
|
||||
Diff с предыдущим
|
||||
</button>
|
||||
<button type="button" onClick={() => onResetDefaults()}>
|
||||
Сбросить к default
|
||||
</button>
|
||||
</div>
|
||||
{diffSummary ? <p className="diff-summary">{diffSummary}</p> : null}
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
import type { QueryState } from "../state/types";
|
||||
|
||||
interface QueryPanelProps {
|
||||
value: QueryState;
|
||||
onChange: (next: QueryState) => void;
|
||||
onApplyBatchFormat: () => void;
|
||||
onNormalize: (saveCase: boolean) => Promise<void> | void;
|
||||
busy: boolean;
|
||||
useMock: boolean;
|
||||
onUseMockChange: (next: boolean) => void;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
export function QueryPanel({
|
||||
value,
|
||||
onChange,
|
||||
onApplyBatchFormat,
|
||||
onNormalize,
|
||||
busy,
|
||||
useMock,
|
||||
onUseMockChange,
|
||||
errorMessage
|
||||
}: QueryPanelProps) {
|
||||
return (
|
||||
<PanelFrame title="Запрос пользователя" subtitle="NDC semantic front-end: нормализуем, но не отвечаем за бухгалтерскую суть.">
|
||||
<div className="grid-two">
|
||||
<label className="full-width">
|
||||
Raw user question
|
||||
<textarea
|
||||
value={value.userQuestion}
|
||||
onChange={(event) => onChange({ ...value, userQuestion: event.target.value })}
|
||||
rows={6}
|
||||
placeholder="Например: По каким покупателям у нас на конец июня висят отгрузки без оплаты..."
|
||||
/>
|
||||
</label>
|
||||
<label className="full-width">
|
||||
Batch queries (`;` separator)
|
||||
<textarea
|
||||
value={value.batchQuestionsRaw}
|
||||
onChange={(event) => onChange({ ...value, batchQuestionsRaw: event.target.value })}
|
||||
onBlur={() => onApplyBatchFormat()}
|
||||
rows={8}
|
||||
placeholder="Вопрос 1; Вопрос 2; Вопрос 3"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Optional period context
|
||||
<input value={value.periodHint} onChange={(event) => onChange({ ...value, periodHint: event.target.value })} />
|
||||
</label>
|
||||
<label>
|
||||
Optional business context
|
||||
<input value={value.businessContext} onChange={(event) => onChange({ ...value, businessContext: event.target.value })} />
|
||||
</label>
|
||||
<label>
|
||||
Optional expected route (eval)
|
||||
<input value={value.expectedRoute} onChange={(event) => onChange({ ...value, expectedRoute: event.target.value })} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="button-row">
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={useMock} onChange={(event) => onUseMockChange(event.target.checked)} />
|
||||
Mock-режим (без вызова OpenAI)
|
||||
</label>
|
||||
<button type="button" onClick={() => onApplyBatchFormat()} disabled={busy || !value.batchQuestionsRaw.trim()}>
|
||||
Применить `;` в переносы
|
||||
</button>
|
||||
<button type="button" onClick={() => onNormalize(false)} disabled={busy || !value.userQuestion.trim()}>
|
||||
{busy ? "Нормализуем..." : "Normalize"}
|
||||
</button>
|
||||
<button type="button" onClick={() => onNormalize(true)} disabled={busy || !value.userQuestion.trim()}>
|
||||
{busy ? "Сохраняем..." : "Normalize + Save as test case"}
|
||||
</button>
|
||||
</div>
|
||||
{errorMessage ? <p className="error-text">{errorMessage}</p> : null}
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
import type { RuntimeRun } from "../state/types";
|
||||
import { JsonView } from "./JsonView";
|
||||
|
||||
interface RuntimePanelProps {
|
||||
runs: RuntimeRun[];
|
||||
selectedRunId: string;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onStartRun: () => Promise<void> | void;
|
||||
onFinishRun: () => Promise<void> | void;
|
||||
onRefreshRuns: () => Promise<void> | void;
|
||||
onRunEval: () => Promise<void> | void;
|
||||
onCopyEvalReport: () => Promise<void> | void;
|
||||
evalBusy: boolean;
|
||||
traceItems: unknown[];
|
||||
evalReport: unknown;
|
||||
}
|
||||
|
||||
export function RuntimePanel({
|
||||
runs,
|
||||
selectedRunId,
|
||||
onSelectRun,
|
||||
onStartRun,
|
||||
onFinishRun,
|
||||
onRefreshRuns,
|
||||
onRunEval,
|
||||
onCopyEvalReport,
|
||||
evalBusy,
|
||||
traceItems,
|
||||
evalReport
|
||||
}: RuntimePanelProps) {
|
||||
return (
|
||||
<PanelFrame
|
||||
title="NDC Run Monitor"
|
||||
subtitle="Важно: кнопка Запустить run создает только run-сущность. Кнопка eval запускает batch-проверку normalizer v2.0.2."
|
||||
>
|
||||
<div className="button-row">
|
||||
<button type="button" onClick={() => onStartRun()}>
|
||||
Запустить run
|
||||
</button>
|
||||
<button type="button" onClick={() => onFinishRun()} disabled={!selectedRunId}>
|
||||
Завершить выбранный run
|
||||
</button>
|
||||
<button type="button" onClick={() => onRefreshRuns()}>
|
||||
Обновить runs
|
||||
</button>
|
||||
<button type="button" onClick={() => onRunEval()} disabled={evalBusy}>
|
||||
{evalBusy ? "Идет eval v2.0.2..." : "Запустить eval v2.0.2"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="runtime-grid">
|
||||
<div className="runtime-runs">
|
||||
{runs.map((run) => (
|
||||
<button
|
||||
key={run.runId}
|
||||
type="button"
|
||||
className={selectedRunId === run.runId ? "history-item selected" : "history-item"}
|
||||
onClick={() => onSelectRun(run.runId)}
|
||||
>
|
||||
<div className="history-row">
|
||||
<strong>{run.status}</strong>
|
||||
<span>{run.runId}</span>
|
||||
</div>
|
||||
<div className="history-row">
|
||||
<span>{run.sessionId}</span>
|
||||
<span>{new Date(run.updatedAt).toLocaleString("ru-RU")}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{runs.length === 0 ? <p className="muted">Нет активных запусков.</p> : null}
|
||||
</div>
|
||||
<div>
|
||||
<h3>Trace выбранного run</h3>
|
||||
<JsonView value={traceItems} />
|
||||
<div className="eval-report-wrap">
|
||||
<h3 style={{ marginTop: 12 }}>Отчет eval</h3>
|
||||
<JsonView value={evalReport ?? { note: "Eval пока не запускался" }} />
|
||||
<button type="button" className="copy-cube-button" title="Скопировать отчет eval" onClick={() => onCopyEvalReport()}>
|
||||
⧉
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelFrame>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user