ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.12.23: декомпозиция deep-turn пайплайна ассистента в runtime-адаптеры

This commit is contained in:
2026-04-10 19:22:40 +03:00
parent 19dbaff741
commit 80d108e506
116 changed files with 13949 additions and 2039 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -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-DNcr9aV9.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-qw2gR5-7.css">
<script type="module" crossorigin src="/assets/index-DMMD5-xN.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BfGkpjEM.css">
</head>
<body>
<div id="root"></div>
+6 -2
View File
@@ -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;
}
+141 -18
View File
@@ -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;