ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.12.23: декомпозиция deep-turn пайплайна ассистента в runtime-адаптеры
This commit is contained in:
@@ -160,6 +160,7 @@ export const apiClient = {
|
||||
rawQuestions?: string;
|
||||
evalTarget?: "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0";
|
||||
compareWithReportFile?: string;
|
||||
analysisDate?: string;
|
||||
}): Promise<{ ok: boolean; report: unknown }> {
|
||||
return request("/eval/run", {
|
||||
method: "POST",
|
||||
@@ -183,7 +184,8 @@ export const apiClient = {
|
||||
caseSetFile: input.caseSetFile,
|
||||
rawQuestions: input.rawQuestions,
|
||||
eval_target: input.evalTarget,
|
||||
compare_with_report_file: input.compareWithReportFile
|
||||
compare_with_report_file: input.compareWithReportFile,
|
||||
analysis_date: input.analysisDate
|
||||
})
|
||||
});
|
||||
},
|
||||
@@ -200,6 +202,7 @@ export const apiClient = {
|
||||
evalTarget?: "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0";
|
||||
compareWithReportFile?: string;
|
||||
questions?: string[];
|
||||
analysisDate?: string;
|
||||
}): Promise<AsyncEvalRunStartResponse> {
|
||||
return request("/eval/run-async/start", {
|
||||
method: "POST",
|
||||
@@ -224,7 +227,8 @@ export const apiClient = {
|
||||
rawQuestions: input.rawQuestions,
|
||||
eval_target: input.evalTarget,
|
||||
compare_with_report_file: input.compareWithReportFile,
|
||||
questions: input.questions
|
||||
questions: input.questions,
|
||||
analysis_date: input.analysisDate
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
@@ -75,15 +75,16 @@ export function AssistantPanel({
|
||||
errorMessage
|
||||
}: AssistantPanelProps) {
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const stickToBottomRef = useRef(true);
|
||||
const copyResetTimerRef = useRef<number | null>(null);
|
||||
const [copyState, setCopyState] = useState<"idle" | "success" | "error">("idle");
|
||||
const [copyModeLabel, setCopyModeLabel] = useState<"чат" | "тех">("чат");
|
||||
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
if (listRef.current && stickToBottomRef.current) {
|
||||
listRef.current.scrollTop = listRef.current.scrollHeight;
|
||||
}
|
||||
}, [conversation, statusText]);
|
||||
}, [conversation]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -111,85 +112,93 @@ export function AssistantPanel({
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelFrame
|
||||
title="Режим ассистента"
|
||||
subtitle="Диалоговый слой поверх normalizer, маршрутизации и factual retrieval."
|
||||
actions={
|
||||
<div className="assistant-panel-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-copy-btn"
|
||||
onClick={() => {
|
||||
void handleCopyConversation("default");
|
||||
}}
|
||||
disabled={conversation.length === 0}
|
||||
title="Экспорт только user-facing чата"
|
||||
>
|
||||
Скопировать чат
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-copy-btn"
|
||||
onClick={() => {
|
||||
void handleCopyConversation("technical");
|
||||
}}
|
||||
disabled={conversation.length === 0}
|
||||
title="Технический экспорт с debug payload"
|
||||
>
|
||||
Скопировать техчат
|
||||
</button>
|
||||
{copyState === "success" ? <span className="assistant-copy-feedback success">Скопировано ({copyModeLabel})</span> : null}
|
||||
{copyState === "error" ? <span className="assistant-copy-feedback error">Ошибка копирования</span> : null}
|
||||
<span className="status-chip">{sessionId ? `session: ${sessionId}` : "новая сессия"}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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>
|
||||
function handleChatScroll(): void {
|
||||
if (!listRef.current) return;
|
||||
const node = listRef.current;
|
||||
const distanceToBottom = node.scrollHeight - node.scrollTop - node.clientHeight;
|
||||
stickToBottomRef.current = distanceToBottom < 16;
|
||||
}
|
||||
|
||||
<div className="assistant-compose">
|
||||
<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-режим
|
||||
return (
|
||||
<PanelFrame className="assistant-panel-frame" title="Режим ассистента">
|
||||
<div className="assistant-live-shell">
|
||||
<div className="assistant-toolbar">
|
||||
<div className="assistant-toolbar-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-copy-btn"
|
||||
onClick={() => {
|
||||
void handleCopyConversation("default");
|
||||
}}
|
||||
disabled={conversation.length === 0}
|
||||
title="Экспорт только user-facing чата"
|
||||
>
|
||||
Скопировать чат
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-copy-btn"
|
||||
onClick={() => {
|
||||
void handleCopyConversation("technical");
|
||||
}}
|
||||
disabled={conversation.length === 0}
|
||||
title="Технический экспорт с debug payload"
|
||||
>
|
||||
Скопировать техчат
|
||||
</button>
|
||||
<button type="button" className="assistant-copy-btn" onClick={() => onClear()} disabled={busy && conversation.length === 0}>
|
||||
Сбросить сессию
|
||||
</button>
|
||||
</div>
|
||||
<div className="assistant-toolbar-meta">
|
||||
{sessionId ? <span className="status-chip">{`session: ${sessionId}`}</span> : null}
|
||||
<div className="assistant-toolbar-meta-right">
|
||||
{statusText ? <span className="assistant-live-status">{statusText}</span> : null}
|
||||
{copyState === "success" ? <span className="assistant-copy-feedback success">Скопировано ({copyModeLabel})</span> : null}
|
||||
{copyState === "error" ? <span className="assistant-copy-feedback error">Ошибка копирования</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
{errorMessage ? <p className="error-text assistant-toolbar-error">{errorMessage}</p> : null}
|
||||
</div>
|
||||
|
||||
<div ref={listRef} className="assistant-chat-list" onScroll={handleChatScroll}>
|
||||
{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">
|
||||
<label className="full-width">
|
||||
Сообщение
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={(event) => onInputChange(event.target.value)}
|
||||
rows={4}
|
||||
placeholder="Введите вопрос к данным компании..."
|
||||
/>
|
||||
</label>
|
||||
<button type="button" onClick={() => onSend()} disabled={busy || !inputValue.trim()}>
|
||||
{busy ? "Выполняю..." : "Отправить"}
|
||||
</button>
|
||||
<button type="button" onClick={() => onClear()} disabled={busy && conversation.length === 0}>
|
||||
Сбросить сессию
|
||||
</button>
|
||||
<div className="button-row assistant-send-row">
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={useMock} onChange={(event) => onUseMockChange(event.target.checked)} />
|
||||
Mock-режим
|
||||
</label>
|
||||
<button type="button" className="assistant-send-btn" onClick={() => onSend()} disabled={busy || !inputValue.trim()}>
|
||||
{busy ? "Выполняю..." : "Отправить"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{statusText ? <p className="diff-summary">{statusText}</p> : null}
|
||||
{errorMessage ? <p className="error-text">{errorMessage}</p> : null}
|
||||
</div>
|
||||
</PanelFrame>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { apiClient } from "../api/client";
|
||||
import type {
|
||||
AssistantConversationItem,
|
||||
AsyncEvalRunJob,
|
||||
AutoGenHistoryRecord,
|
||||
AutoGenMode,
|
||||
@@ -18,6 +19,7 @@ import type {
|
||||
ManualCaseDecision,
|
||||
PromptState
|
||||
} from "../state/types";
|
||||
import { AssistantPanel } from "./AssistantPanel";
|
||||
import { JsonView } from "./JsonView";
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
|
||||
@@ -91,6 +93,7 @@ const LIVE_RUN_ID_PREFIX = "__live__:";
|
||||
|
||||
const AUTORUNS_UI_CONFIG_KEY = "ndc_autoruns_ui_config_v1";
|
||||
const AUTORUNS_SAVE_EVENT = "ndc-autoruns-save";
|
||||
const ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"];
|
||||
|
||||
const AUTOGEN_PERSONALITIES: AutoGenPersonalityDefinition[] = [
|
||||
{
|
||||
@@ -113,6 +116,7 @@ function buildDefaultPersonalityPrompts(
|
||||
|
||||
interface AutoRunsUiConfig {
|
||||
filters?: Partial<AutoRunsFilters>;
|
||||
analysisDate?: string;
|
||||
autoGenSettings?: {
|
||||
mode?: AutoGenMode;
|
||||
count?: number;
|
||||
@@ -134,6 +138,11 @@ const DEFAULT_AUTOGEN_SETTINGS: AutoGenSettingsState = {
|
||||
generatedBy: "manual_reviewer"
|
||||
};
|
||||
|
||||
function normalizeAnalysisDateInput(value: string): string {
|
||||
const normalized = String(value ?? "").trim();
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : "";
|
||||
}
|
||||
|
||||
function dateToInputValue(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
@@ -246,6 +255,7 @@ function buildLiveRunSummary(job: AsyncEvalRunJob): AutoRunSummary {
|
||||
llm_provider: null,
|
||||
model: null,
|
||||
use_mock: null,
|
||||
analysis_date: job.report_summary?.analysis_date ?? job.analysis_date ?? null,
|
||||
prompt_version: null,
|
||||
schema_version: null,
|
||||
suite_id: job.case_set_file,
|
||||
@@ -314,7 +324,8 @@ function buildLiveRunDetail(job: AsyncEvalRunJob, requestedCaseId: string): {
|
||||
run_id: job.report_summary.run_id,
|
||||
run_timestamp: job.report_summary.run_timestamp,
|
||||
score_index: job.report_summary.score_index,
|
||||
cases_total: job.report_summary.cases_total
|
||||
cases_total: job.report_summary.cases_total,
|
||||
analysis_date: job.report_summary.analysis_date ?? job.analysis_date ?? null
|
||||
}
|
||||
: {}
|
||||
};
|
||||
@@ -411,6 +422,7 @@ export function AutoRunsHistoryPanel({
|
||||
...DEFAULT_FILTERS,
|
||||
fromLocal: defaultFromDateValue()
|
||||
});
|
||||
const [analysisDate, setAnalysisDate] = useState("");
|
||||
const [history, setHistory] = useState<AutoRunHistoryResponse | null>(null);
|
||||
const [runDetail, setRunDetail] = useState<AutoRunDetailResponse | null>(null);
|
||||
const [dialog, setDialog] = useState<AutoRunDialogResponse | null>(null);
|
||||
@@ -439,6 +451,13 @@ export function AutoRunsHistoryPanel({
|
||||
const [annotationsBusy, setAnnotationsBusy] = useState(false);
|
||||
const [annotationResolutionBusyId, setAnnotationResolutionBusyId] = useState("");
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [assistantLiveSessionId, setAssistantLiveSessionId] = useState("");
|
||||
const [assistantLiveConversation, setAssistantLiveConversation] = useState<AssistantConversationItem[]>([]);
|
||||
const [assistantLiveInput, setAssistantLiveInput] = useState("");
|
||||
const [assistantLiveUseMock, setAssistantLiveUseMock] = useState(false);
|
||||
const [assistantLiveBusy, setAssistantLiveBusy] = useState(false);
|
||||
const [assistantLiveStatus, setAssistantLiveStatus] = useState("");
|
||||
const [assistantLiveError, setAssistantLiveError] = useState("");
|
||||
const [limitInput, setLimitInput] = useState(String(DEFAULT_FILTERS.limit));
|
||||
const [autogenCountInput, setAutogenCountInput] = useState(String(DEFAULT_AUTOGEN_SETTINGS.count));
|
||||
const [commentModal, setCommentModal] = useState<CommentModalState>({
|
||||
@@ -465,8 +484,6 @@ export function AutoRunsHistoryPanel({
|
||||
[autoGenHistory, selectedAutogenGenerationId]
|
||||
);
|
||||
|
||||
const activeRunSummary: AutoRunSummary | null =
|
||||
history?.items.find((item) => item.run_id === selectedRunId) ?? runDetail?.run ?? null;
|
||||
const activeCase = runDetail ? getSelectedCase(runDetail.cases, selectedCaseId) : null;
|
||||
const visibleAnnotations = useMemo(
|
||||
() => (hideResolvedAnnotations ? annotations.filter((item) => !item.resolved) : annotations),
|
||||
@@ -509,6 +526,81 @@ export function AutoRunsHistoryPanel({
|
||||
[onLog]
|
||||
);
|
||||
|
||||
function startAssistantLiveStatusTicker(): () => void {
|
||||
let index = 0;
|
||||
setAssistantLiveStatus(ASSISTANT_STAGES[0]);
|
||||
const timer = window.setInterval(() => {
|
||||
index = Math.min(index + 1, ASSISTANT_STAGES.length - 1);
|
||||
setAssistantLiveStatus(ASSISTANT_STAGES[index]);
|
||||
}, 650);
|
||||
return () => window.clearInterval(timer);
|
||||
}
|
||||
|
||||
const resetAssistantLiveSession = useCallback(() => {
|
||||
setAssistantLiveSessionId("");
|
||||
setAssistantLiveConversation([]);
|
||||
setAssistantLiveInput("");
|
||||
setAssistantLiveStatus("");
|
||||
setAssistantLiveError("");
|
||||
log("Live-чат ассистента в истории автопрогонов сброшен.");
|
||||
}, [log]);
|
||||
|
||||
const sendAssistantLiveMessage = useCallback(async () => {
|
||||
const userMessage = assistantLiveInput.trim();
|
||||
if (!userMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAssistantLiveBusy(true);
|
||||
setAssistantLiveError("");
|
||||
setAssistantLiveInput("");
|
||||
setAssistantLiveConversation((prev) => [
|
||||
...prev,
|
||||
{
|
||||
message_id: `autoruns-live-${Date.now()}`,
|
||||
session_id: assistantLiveSessionId || "pending",
|
||||
role: "user",
|
||||
text: userMessage,
|
||||
reply_type: null,
|
||||
created_at: new Date().toISOString(),
|
||||
trace_id: null,
|
||||
debug: null
|
||||
}
|
||||
]);
|
||||
|
||||
const stopTicker = startAssistantLiveStatusTicker();
|
||||
try {
|
||||
const response = await apiClient.sendAssistantMessage({
|
||||
connection,
|
||||
prompts,
|
||||
userMessage,
|
||||
sessionId: assistantLiveSessionId || undefined,
|
||||
promptVersion: assistantPromptVersion,
|
||||
useMock: assistantLiveUseMock
|
||||
});
|
||||
setAssistantLiveSessionId(response.session_id);
|
||||
setAssistantLiveConversation(response.conversation);
|
||||
setAssistantLiveStatus("Ответ готов");
|
||||
log(`Live-ответ ассистента получен: trace=${response.debug.trace_id}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setAssistantLiveError(message);
|
||||
setAssistantLiveStatus("Ошибка ассистента");
|
||||
log(`Live-чат ассистента: ошибка отправки сообщения: ${message}`);
|
||||
} finally {
|
||||
stopTicker();
|
||||
setAssistantLiveBusy(false);
|
||||
}
|
||||
}, [
|
||||
assistantLiveInput,
|
||||
assistantLiveSessionId,
|
||||
assistantLiveUseMock,
|
||||
assistantPromptVersion,
|
||||
connection,
|
||||
log,
|
||||
prompts
|
||||
]);
|
||||
|
||||
const commitLimitInput = useCallback(
|
||||
(raw: string) => {
|
||||
const normalized = raw.trim();
|
||||
@@ -933,6 +1025,7 @@ export function AutoRunsHistoryPanel({
|
||||
}
|
||||
|
||||
const useMockForRun = filters.useMock === "true";
|
||||
const effectiveAnalysisDate = normalizeAnalysisDateInput(analysisDate);
|
||||
const payload = await apiClient.startEvalRunAsync({
|
||||
connection,
|
||||
prompts,
|
||||
@@ -941,7 +1034,8 @@ export function AutoRunsHistoryPanel({
|
||||
caseSetFile: generation.saved_case_set_file ?? undefined,
|
||||
useMock: useMockForRun,
|
||||
evalTarget: "assistant_stage1",
|
||||
questions: questionsForRun
|
||||
questions: questionsForRun,
|
||||
analysisDate: effectiveAnalysisDate || undefined
|
||||
});
|
||||
|
||||
const liveJob = payload.job;
|
||||
@@ -955,7 +1049,8 @@ export function AutoRunsHistoryPanel({
|
||||
|
||||
log(
|
||||
`Запущен async-прогон job=${liveJob.job_id}, run_id=${liveJob.run_id}, вопросов=${questionsForRun.length}` +
|
||||
(generation.saved_case_set_file ? `, base_case_set=${generation.saved_case_set_file}` : "")
|
||||
(generation.saved_case_set_file ? `, base_case_set=${generation.saved_case_set_file}` : "") +
|
||||
(effectiveAnalysisDate ? `, analysis_date=${effectiveAnalysisDate}` : ", analysis_date=current_state")
|
||||
);
|
||||
void pollAsyncJobStatus(liveJob.job_id);
|
||||
} catch (error) {
|
||||
@@ -965,6 +1060,7 @@ export function AutoRunsHistoryPanel({
|
||||
setAutogenRunBusy(false);
|
||||
}
|
||||
}, [
|
||||
analysisDate,
|
||||
assistantPromptVersion,
|
||||
connection,
|
||||
editableGeneratedQuestions,
|
||||
@@ -1235,6 +1331,9 @@ export function AutoRunsHistoryPanel({
|
||||
limit: typeof savedFilters.limit === "number" ? Math.max(1, Math.min(500, savedFilters.limit)) : prev.limit
|
||||
}));
|
||||
}
|
||||
if (typeof parsed.analysisDate === "string") {
|
||||
setAnalysisDate(normalizeAnalysisDateInput(parsed.analysisDate));
|
||||
}
|
||||
if (parsed.autoGenSettings) {
|
||||
setAutoGenSettings((prev) => {
|
||||
const nextPrompts: Record<string, string> = {
|
||||
@@ -1290,6 +1389,7 @@ export function AutoRunsHistoryPanel({
|
||||
const saveUiConfig = useCallback(() => {
|
||||
const payload: AutoRunsUiConfig = {
|
||||
filters,
|
||||
analysisDate,
|
||||
autoGenSettings: {
|
||||
mode: autoGenSettings.mode,
|
||||
count: autoGenSettings.count,
|
||||
@@ -1302,7 +1402,7 @@ export function AutoRunsHistoryPanel({
|
||||
hideResolvedAnnotations
|
||||
};
|
||||
localStorage.setItem(AUTORUNS_UI_CONFIG_KEY, JSON.stringify(payload));
|
||||
}, [annotationDecisionFilter, autoGenSettings, filters, hideResolvedAnnotations]);
|
||||
}, [analysisDate, annotationDecisionFilter, autoGenSettings, filters, hideResolvedAnnotations]);
|
||||
|
||||
useEffect(() => {
|
||||
const onSave = () => {
|
||||
@@ -1542,6 +1642,25 @@ export function AutoRunsHistoryPanel({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="autoruns-form-grid">
|
||||
<label>
|
||||
Дата анализа (срез)
|
||||
<input
|
||||
type="date"
|
||||
value={analysisDate}
|
||||
onChange={(event) => setAnalysisDate(normalizeAnalysisDateInput(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<div className="button-row">
|
||||
<button type="button" className="tab" disabled={!analysisDate} onClick={() => setAnalysisDate("")}>
|
||||
Сбросить дату среза
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="muted">
|
||||
Если дата среза задана, автопрогон анализирует данные на эту дату. Если поле пустое, используется текущее состояние.
|
||||
</p>
|
||||
|
||||
<div className="button-row">
|
||||
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
|
||||
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
|
||||
@@ -1704,6 +1823,7 @@ export function AutoRunsHistoryPanel({
|
||||
<div className="autoruns-run-meta">
|
||||
режим={run.mode ?? "нет данных"} | mock={String(run.use_mock)}
|
||||
</div>
|
||||
<div className="autoruns-run-meta">analysis_date={run.analysis_date ?? "current_state"}</div>
|
||||
{run.llm_provider || run.model ? (
|
||||
<div className="autoruns-run-meta">
|
||||
llm={run.llm_provider ?? "нет данных"} | модель={run.model ?? "нет данных"}
|
||||
@@ -1879,39 +1999,21 @@ export function AutoRunsHistoryPanel({
|
||||
</section>
|
||||
|
||||
{showAssistantMode ? (
|
||||
<section className="autoruns-col">
|
||||
<div className="autoruns-col-header">
|
||||
<h3>Режим ассистента</h3>
|
||||
</div>
|
||||
<div className="autoruns-meta-list">
|
||||
<div>
|
||||
<span>источник:</span>
|
||||
<strong>{dialog?.source ?? "нет данных"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>сессия:</span>
|
||||
<strong>{dialog?.session_id ?? "нет данных"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>контур прогона:</span>
|
||||
<strong>{activeRunSummary?.eval_target ?? "нет данных"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>оценка прогона:</span>
|
||||
<strong>{formatScore(activeRunSummary?.score_index ?? null)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>комментарии:</span>
|
||||
<strong>{runDetail?.annotations_summary?.total ?? 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<h4>Пакет режима ассистента</h4>
|
||||
<JsonView value={dialog?.assistant_mode ?? { note: "assistant_mode недоступен" }} />
|
||||
<h4 style={{ marginTop: 12 }}>Проверки кейса</h4>
|
||||
<JsonView value={activeCase?.checks ?? { note: "checks недоступен" }} />
|
||||
<h4 style={{ marginTop: 12 }}>Сабскор метрик</h4>
|
||||
<JsonView value={activeCase?.metric_subscores ?? { note: "metric_subscores недоступен" }} />
|
||||
</section>
|
||||
<div className="autoruns-col autoruns-assistant-live-col">
|
||||
<AssistantPanel
|
||||
sessionId={assistantLiveSessionId}
|
||||
conversation={assistantLiveConversation}
|
||||
inputValue={assistantLiveInput}
|
||||
onInputChange={setAssistantLiveInput}
|
||||
useMock={assistantLiveUseMock}
|
||||
onUseMockChange={setAssistantLiveUseMock}
|
||||
onSend={sendAssistantLiveMessage}
|
||||
onClear={resetAssistantLiveSession}
|
||||
busy={assistantLiveBusy}
|
||||
statusText={assistantLiveStatus}
|
||||
errorMessage={assistantLiveError}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showDecompositionMode ? (
|
||||
|
||||
@@ -96,6 +96,7 @@ export interface AutoRunSummary {
|
||||
llm_provider: string | null;
|
||||
model: string | null;
|
||||
use_mock: boolean | null;
|
||||
analysis_date?: string | null;
|
||||
prompt_version: string | null;
|
||||
schema_version: string | null;
|
||||
suite_id: string | null;
|
||||
@@ -367,6 +368,7 @@ export interface AsyncEvalRunJob {
|
||||
eval_target: AutoRunTarget;
|
||||
run_id: string;
|
||||
case_set_file: string | null;
|
||||
analysis_date?: string | null;
|
||||
total_cases: number;
|
||||
completed_cases: number;
|
||||
error: string | null;
|
||||
@@ -376,6 +378,7 @@ export interface AsyncEvalRunJob {
|
||||
run_timestamp: string | null;
|
||||
score_index: number | null;
|
||||
cases_total: number | null;
|
||||
analysis_date?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -249,18 +249,57 @@ body,
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
}
|
||||
|
||||
.assistant-panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
.assistant-toolbar {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-toolbar-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-toolbar-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-toolbar-meta-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.assistant-live-status {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.assistant-toolbar-error {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.assistant-copy-btn {
|
||||
width: 100%;
|
||||
justify-self: stretch;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--text-main);
|
||||
font-size: 0.6rem;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
letter-spacing: 0;
|
||||
padding: 6px 8px;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
@@ -402,16 +441,23 @@ button:disabled {
|
||||
}
|
||||
|
||||
.assistant-chat-list {
|
||||
max-height: 420px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 4px;
|
||||
overscroll-behavior: contain;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
border-radius: 12px;
|
||||
background: rgb(var(--rgb-surface-horizontal));
|
||||
}
|
||||
|
||||
.assistant-chat-list .assistant-msg:first-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.assistant-empty {
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
@@ -420,32 +466,45 @@ button:disabled {
|
||||
.assistant-msg {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: rgb(var(--rgb-surface-main));
|
||||
padding: 10px;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
padding: 8px 10px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.assistant-msg.user {
|
||||
margin-left: 12%;
|
||||
border-color: transparent;
|
||||
background: rgb(var(--rgb-surface-focus));
|
||||
background: rgb(var(--rgb-active));
|
||||
color: rgb(var(--rgb-active-text));
|
||||
}
|
||||
|
||||
.assistant-msg.assistant {
|
||||
margin-right: 12%;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.assistant-msg.user .assistant-msg-head {
|
||||
color: rgba(var(--rgb-active-text), 0.9);
|
||||
}
|
||||
|
||||
.assistant-msg.user .assistant-msg-body {
|
||||
color: rgb(var(--rgb-active-text));
|
||||
}
|
||||
|
||||
.assistant-msg-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.78rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.assistant-msg-body {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.45;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.35;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.assistant-trace {
|
||||
@@ -465,11 +524,63 @@ button:disabled {
|
||||
}
|
||||
|
||||
.assistant-compose {
|
||||
margin-top: 12px;
|
||||
margin-top: 0;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.assistant-send-row {
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.assistant-send-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.app-root-autoruns .assistant-panel-frame .panel-header {
|
||||
position: sticky;
|
||||
top: -12px;
|
||||
z-index: 8;
|
||||
margin: -12px -12px 0;
|
||||
padding: 12px 12px 10px;
|
||||
background: rgb(var(--rgb-surface-main));
|
||||
}
|
||||
|
||||
.app-root-autoruns .assistant-panel-frame {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.app-root-autoruns .assistant-panel-frame .panel-body {
|
||||
flex: 1 1 auto;
|
||||
padding: 0 12px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-root-autoruns .assistant-panel-frame .assistant-live-shell {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: rgb(var(--rgb-background));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.app-root-autoruns .assistant-panel-frame .assistant-chat-list {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.app-root-autoruns .assistant-panel-frame .panel-header h2 {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.json-view {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
@@ -630,6 +741,18 @@ button:disabled {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.autoruns-assistant-live-col {
|
||||
background: rgb(var(--rgb-surface-main));
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
scrollbar-gutter: auto;
|
||||
}
|
||||
|
||||
.autoruns-assistant-live-col .panel-frame {
|
||||
height: 100%;
|
||||
background: rgb(var(--rgb-surface-main));
|
||||
}
|
||||
|
||||
.autoruns-col h3 {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
|
||||
Reference in New Issue
Block a user