import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEvent, type SyntheticEvent } from "react"; import { apiClient } from "../api/client"; import type { AssistantConversationItem, AssistantAnnotationRecord, AssistantSelectionChip, AsyncEvalRunJob, AutoGenHistoryRecord, AutoGenMode, AutoRunAnnotationListItem, AutoRunAnnotationRecord, AutoRunCaseSummary, AutoRunDetailResponse, AutoRunDialogMessage, AutoRunDialogResponse, AutoRunDomainCoverage, AutoRunHistoryResponse, AutoRunPostAnalysisResponse, AutoRunSummary, ConnectionState, ManualCaseDecision, PromptState } from "../state/types"; import { AssistantPanel } from "./AssistantPanel"; import { ConnectionPanel } from "./ConnectionPanel"; import { JsonView } from "./JsonView"; import { PanelFrame } from "./PanelFrame"; import { PromptPanel } from "./PromptPanel"; interface AutoRunsHistoryPanelProps { connection: ConnectionState; modelOptions: string[]; modelsBusy: boolean; connectionStatus: string; connectionBusy: boolean; onConnectionChange: (next: ConnectionState) => void; onReloadModels: () => Promise | void; onSaveLocalConfig: () => void; onTestConnection: () => Promise | void; prompts: PromptState; onPromptsChange: (next: PromptState) => void; promptPresets: Array<{ id: string; name: string; prompt_version: string; systemPrompt: string; developerPrompt: string; domainPrompt: string; schemaNotes?: string; fewShotExamples?: string; }>; selectedPresetId: string; onSelectPreset: (id: string) => void; onLoadPreset: () => void; onSavePreset: () => void; onResetDefaults: () => void; onDiffPrevious: () => void; presetName: string; onPresetNameChange: (name: string) => void; diffSummary: string; assistantPromptVersion: string; decompositionPromptVersion: string; showSettingsMode: boolean; showAutoRunsMode: boolean; showAssistantMode: boolean; showProgressMode: boolean; showCommentsMode: boolean; onLog?: (message: string) => void; } type UseMockFilter = "any" | "true" | "false"; type AutoGenPersonalityId = string; interface AutoGenPersonalityDefinition { id: AutoGenPersonalityId; label: string; domain: string; defaultPrompt: string; } interface AutoRunsFilters { fromLocal: string; toLocal: string; target: string; mode: string; useMock: UseMockFilter; promptContains: string; limit: number; } interface CommentModalState { open: boolean; caseId: string; caseMessageIndex: number; messageIndex: number; rating: number; comment: string; manualCaseDecision: ManualCaseDecision; annotationAuthor: string; saving: boolean; error: string; } interface AssistantLiveCommentModalState { open: boolean; messageIndex: number; rating: number; comment: string; annotationAuthor: string; saving: boolean; error: string; } interface AssistantLiveSaveModalState { open: boolean; title: string; saving: boolean; error: string; } interface SavedSessionQuestionDeleteModalState { open: boolean; generationId: string; questionIndex: number; questionText: string; saving: boolean; error: string; } interface AutoGenDeleteModalState { open: boolean; generationId: string; title: string; saving: boolean; error: string; } interface AutoGenSettingsState { mode: AutoGenMode; count: number; personalityId: AutoGenPersonalityId; personalityPrompts: Record; persistToEvalCases: boolean; generatedBy: string; } const DEFAULT_FILTERS: AutoRunsFilters = { fromLocal: "", toLocal: "", target: "all", mode: "all", useMock: "any", promptContains: "", limit: 120 }; const DEFAULT_MANUAL_DECISION: ManualCaseDecision = "needs_dialog_policy_fix"; const ALL_CASES_ID = "__all__"; 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 = ["Анализ запроса", "Получение данных", "Подготовка ответа"]; function buildAssistantLiveFollowupMessage(inputValue: string, selectedChip: AssistantSelectionChip | null): string { const trimmedInput = inputValue.trim(); if (!trimmedInput) { return ""; } if (!selectedChip) { return trimmedInput; } const normalizedInput = trimmedInput.toLowerCase(); const selectionAnchor = selectedChip.anchor_text.trim(); const normalizedSelection = selectionAnchor.toLowerCase(); if (normalizedSelection && normalizedInput.includes(normalizedSelection)) { return trimmedInput; } return `По выбранному объекту "${selectionAnchor}": ${trimmedInput}`; } const AUTOGEN_PERSONALITIES: AutoGenPersonalityDefinition[] = [ { id: "general", label: "Общий контур", domain: "", defaultPrompt: "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл." } ]; function buildDefaultPersonalityPrompts( personalities: AutoGenPersonalityDefinition[] = AUTOGEN_PERSONALITIES ): Record { return personalities.reduce((acc, item) => { acc[item.id] = item.defaultPrompt; return acc; }, {} as Record); } interface AutoRunsUiConfig { filters?: Partial; analysisDate?: string; autogenPersonalityPromptHeight?: number; groupsExpanded?: { filters?: boolean; generationContext?: boolean; autogen?: boolean; savedSessions?: boolean; }; autoGenSettings?: { mode?: AutoGenMode; count?: number; personalityId?: string; personalityPrompts?: Record; persistToEvalCases?: boolean; generatedBy?: string; }; annotationDecisionFilter?: ManualCaseDecision | "all"; hideResolvedAnnotations?: boolean; } type UnifiedCommentListItem = | { source: "autorun"; key: string; updated_at: string; rating: number; autorun: AutoRunAnnotationListItem; assistant: null; } | { source: "assistant_live"; key: string; updated_at: string; rating: number; autorun: null; assistant: AssistantAnnotationRecord; }; const DEFAULT_AUTOGEN_SETTINGS: AutoGenSettingsState = { mode: "codex_creative", count: 24, personalityId: "general", personalityPrompts: buildDefaultPersonalityPrompts(), persistToEvalCases: true, generatedBy: "manual_reviewer" }; function normalizeAnalysisDateInput(value: string): string { const normalized = String(value ?? "").trim(); return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : ""; } function clampAutogenPromptHeight(value: number | null | undefined): number { const numeric = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : 160; return Math.max(110, Math.min(520, numeric)); } 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 | null): string { if (!iso) return "нет данных"; const parsed = Date.parse(iso); if (!Number.isFinite(parsed)) return iso; return new Date(parsed).toLocaleString("ru-RU"); } function formatDialogStepTag(message: AutoRunDialogMessage): string | null { const localIndex = typeof message.case_message_index === "number" ? message.case_message_index : typeof message.message_index === "number" ? message.message_index : null; if (localIndex === null || localIndex < 0) { return null; } const turnNumber = Math.floor(localIndex / 2) + 1; const turnLabel = String(turnNumber).padStart(3, "0"); const roleLabel = message.role === "assistant" ? "ответ" : "вопрос"; return `${turnLabel} ${roleLabel}`; } function formatAutoGenModeLabel(mode: AutoGenMode): string { if (mode === "saved_user_sessions") { return "Пользовательские сессии"; } return mode; } function isAgentSemanticGeneration(item: AutoGenHistoryRecord | null | undefined): boolean { if (!item) { return false; } if (item.context?.agent_run === true) { return true; } if (item.context?.saved_case_set_kind === "agent_semantic_scenario") { return true; } return typeof item.title === "string" && item.title.trim().toUpperCase().startsWith("AGENT"); } function formatAutoGenGenerationTitle(item: AutoGenHistoryRecord): string { const fallback = item.title ?? formatDateTime(item.created_at); if (isAgentSemanticGeneration(item) && !fallback.trim().toUpperCase().startsWith("AGENT")) { return `AGENT | ${fallback}`; } return fallback; } function buildSavedSessionDefaultTitle(items: AssistantConversationItem[]): string { const lastMessage = items[items.length - 1]; const timestamp = formatDateTime(lastMessage?.created_at ?? new Date().toISOString()); return `Ручная сессия ${timestamp}`; } 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 "нет данных"; 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 renderRatingDots(rating: number): string { const safe = Math.max(1, Math.min(5, Math.round(rating))); return `${"●".repeat(safe)}${"○".repeat(5 - safe)}`; } function renderCoverageRows(items: AutoRunDomainCoverage[]) { if (items.length === 0) { return

Покрытие доменов пока не сформировано.

; } return (
{items.map((item) => { const percent = toPercent(item.closed_cases, item.total_cases); return (
{item.domain} {item.closed_cases}/{item.total_cases} ({percent}%)
); })}
); } function toLiveRunId(jobId: string): string { return `${LIVE_RUN_ID_PREFIX}${jobId}`; } function isLiveRunId(runId: string): boolean { return runId.startsWith(LIVE_RUN_ID_PREFIX); } function jobIdFromLiveRunId(runId: string): string { return runId.startsWith(LIVE_RUN_ID_PREFIX) ? runId.slice(LIVE_RUN_ID_PREFIX.length) : ""; } function buildLiveRunSummary(job: AsyncEvalRunJob): AutoRunSummary { const timestamp = job.report_summary?.run_timestamp ?? job.created_at; const openCases = Math.max(0, job.total_cases - job.completed_cases); const liveRunId = toLiveRunId(job.job_id); return { run_id: liveRunId, eval_target: job.eval_target, run_timestamp: timestamp, mode: "single-pass-strict", 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, cases_total: job.total_cases, requests_total: null, report_path: `async_job:${job.job_id}`, score_index: job.report_summary?.score_index ?? null, blocking_failures: 0, quality_failures: 0, closed_cases: job.completed_cases, open_cases: openCases, domain_coverage: [ { domain: "runtime", total_cases: job.total_cases, closed_cases: job.completed_cases } ] }; } function buildLiveRunDetail(job: AsyncEvalRunJob, requestedCaseId: string): { detail: AutoRunDetailResponse; dialog: AutoRunDialogResponse; caseId: string; } { const summary = buildLiveRunSummary(job); const cases = job.cases.map((item) => ({ case_id: item.case_id, domain: null, query_class: null, status: item.status === "completed" ? "closed" : item.status === "failed" ? "open" : "unknown", score_index: null, trace_id: null, reply_type: null, session_id: `${job.run_id}-${item.case_id}`, dialog_available: item.messages.length > 0, commented_count: 0, latest_annotation_at: null, avg_rating: null, checks: null, metric_subscores: null })); const hasCase = requestedCaseId !== ALL_CASES_ID && cases.some((item) => item.case_id === requestedCaseId); const caseId = hasCase ? requestedCaseId : cases.length > 0 ? ALL_CASES_ID : ""; const detail: AutoRunDetailResponse = { ok: true, run: summary, coverage: { closed_cases: job.completed_cases, open_cases: Math.max(0, job.total_cases - job.completed_cases), domain_coverage: [ { domain: "runtime", total_cases: job.total_cases, closed_cases: job.completed_cases } ] }, cases, annotations_summary: { total: 0 }, report: job.report_summary ? { 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, analysis_date: job.report_summary.analysis_date ?? job.analysis_date ?? null } : {} }; const messages: AutoRunDialogMessage[] = []; let globalIndex = 0; if (caseId === ALL_CASES_ID) { for (const caseItem of job.cases) { for (let index = 0; index < caseItem.messages.length; index += 1) { const message = caseItem.messages[index]; messages.push({ ...message, message_index: globalIndex, case_id: caseItem.case_id, case_message_index: index, commented: false, annotation: null }); globalIndex += 1; } } } else if (caseId) { const selected = job.cases.find((item) => item.case_id === caseId) ?? null; for (let index = 0; index < (selected?.messages.length ?? 0); index += 1) { const message = selected?.messages[index]; if (!message) continue; messages.push({ ...message, message_index: index, case_id: caseId, case_message_index: index, commented: false, annotation: null }); } } const dialog: AutoRunDialogResponse = { ok: true, run_id: summary.run_id, case_id: caseId, source: "assistant_session", session_id: caseId === ALL_CASES_ID ? `${job.run_id}::__all__` : `${job.run_id}-${caseId}`, messages, decomposition: [], assistant_mode: { status: job.status, completed_cases: job.completed_cases, total_cases: job.total_cases }, annotations: [] }; return { detail, dialog, caseId }; } function CommentBubbleIcon({ commented }: { commented: boolean }) { const className = commented ? "comment-icon-svg commented" : "comment-icon-svg"; return ( ); } function CommentResolvedIcon({ resolved }: { resolved: boolean }) { return ( ); } function CopyOutlineIcon() { return ( ); } function QuestionGripIcon() { return ( ); } function CardChevronIcon({ expanded }: { expanded: boolean }) { return ( ); } function CardLaunchIcon() { return ( ); } function CardStopIcon() { return ( ); } function GroupChevronIcon({ expanded }: { expanded: boolean }) { return ( ); } export function AutoRunsHistoryPanel({ connection, modelOptions, modelsBusy, connectionStatus, connectionBusy, onConnectionChange, onReloadModels, onSaveLocalConfig, onTestConnection, prompts, onPromptsChange, promptPresets, selectedPresetId, onSelectPreset, onLoadPreset, onSavePreset, onResetDefaults, onDiffPrevious, presetName, onPresetNameChange, diffSummary, assistantPromptVersion, decompositionPromptVersion, showSettingsMode, showAutoRunsMode, showAssistantMode, showProgressMode, showCommentsMode, onLog }: AutoRunsHistoryPanelProps) { const [filters, setFilters] = useState({ ...DEFAULT_FILTERS, fromLocal: defaultFromDateValue() }); const [analysisDate, setAnalysisDate] = useState(""); const [history, setHistory] = useState(null); const [runDetail, setRunDetail] = useState(null); const [dialog, setDialog] = useState(null); const [annotations, setAnnotations] = useState([]); const [annotationDecisionFilter, setAnnotationDecisionFilter] = useState("all"); const [hideResolvedAnnotations, setHideResolvedAnnotations] = useState(false); const [manualDecisionSchema, setManualDecisionSchema] = useState | null>(null); const [availableManualDecisions, setAvailableManualDecisions] = useState([]); const [selectedAnnotationId, setSelectedAnnotationId] = useState(""); const [selectedRunId, setSelectedRunId] = useState(""); const [selectedCaseId, setSelectedCaseId] = useState(""); const [autogenPersonalities, setAutogenPersonalities] = useState(AUTOGEN_PERSONALITIES); const [autoGenSettings, setAutoGenSettings] = useState(DEFAULT_AUTOGEN_SETTINGS); const [autoGenHistory, setAutoGenHistory] = useState([]); const [selectedAutogenGenerationId, setSelectedAutogenGenerationId] = useState(""); const [expandedSavedSessionGenerationId, setExpandedSavedSessionGenerationId] = useState(""); const [editableGeneratedQuestions, setEditableGeneratedQuestions] = useState([]); const [generatedQuestionsBusy, setGeneratedQuestionsBusy] = useState(false); const [editingQuestionIndex, setEditingQuestionIndex] = useState(null); const [editingQuestionDraft, setEditingQuestionDraft] = useState(""); const [draggingQuestionIndex, setDraggingQuestionIndex] = useState(null); const [dragOverQuestionIndex, setDragOverQuestionIndex] = useState(null); const [activeAsyncJob, setActiveAsyncJob] = useState(null); const [postAnalysis, setPostAnalysis] = useState(null); const [autoGenBusy, setAutoGenBusy] = useState(false); const [autogenRunBusy, setAutogenRunBusy] = useState(false); const [postAnalysisBusy, setPostAnalysisBusy] = useState(false); const [autogenHistoryBusy, setAutogenHistoryBusy] = useState(false); const [historyBusy, setHistoryBusy] = useState(false); const [detailBusy, setDetailBusy] = useState(false); const [dialogBusy, setDialogBusy] = useState(false); const [annotationsBusy, setAnnotationsBusy] = useState(false); const [annotationResolutionBusyId, setAnnotationResolutionBusyId] = useState(""); const [errorText, setErrorText] = useState(""); const [assistantLiveSessionId, setAssistantLiveSessionId] = useState(""); const [assistantLiveConversation, setAssistantLiveConversation] = useState([]); const [assistantLiveAnnotations, setAssistantLiveAnnotations] = useState([]); const [assistantLiveInput, setAssistantLiveInput] = useState(""); const [assistantLiveSelectedChip, setAssistantLiveSelectedChip] = useState(null); 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 [autogenPersonalityPromptHeight, setAutogenPersonalityPromptHeight] = useState(160); const [runningAutogenGenerationId, setRunningAutogenGenerationId] = useState(""); const [autogenStopBusy, setAutogenStopBusy] = useState(false); const [filtersGroupExpanded, setFiltersGroupExpanded] = useState(true); const [generationContextGroupExpanded, setGenerationContextGroupExpanded] = useState(true); const [autogenGroupExpanded, setAutogenGroupExpanded] = useState(true); const [savedSessionsGroupExpanded, setSavedSessionsGroupExpanded] = useState(true); const [commentModal, setCommentModal] = useState({ open: false, caseId: "", caseMessageIndex: -1, messageIndex: -1, rating: 3, comment: "", manualCaseDecision: DEFAULT_MANUAL_DECISION, annotationAuthor: "manual_reviewer", saving: false, error: "" }); const [assistantLiveCommentModal, setAssistantLiveCommentModal] = useState({ open: false, messageIndex: -1, rating: 3, comment: "", annotationAuthor: "manual_reviewer", saving: false, error: "" }); const [assistantLiveSaveModal, setAssistantLiveSaveModal] = useState({ open: false, title: "", saving: false, error: "" }); const [savedSessionQuestionDeleteModal, setSavedSessionQuestionDeleteModal] = useState({ open: false, generationId: "", questionIndex: -1, questionText: "", saving: false, error: "" }); const [autoGenDeleteModal, setAutoGenDeleteModal] = useState({ open: false, generationId: "", title: "", saving: false, error: "" }); const initialLoadDoneRef = useRef(false); const asyncJobPollTimerRef = useRef(null); const questionEditorRef = useRef(null); const isSavedUserSessionsMode = autoGenSettings.mode === "saved_user_sessions"; const selectedPersonality = useMemo( () => autogenPersonalities.find((item) => item.id === autoGenSettings.personalityId) ?? autogenPersonalities[0] ?? AUTOGEN_PERSONALITIES[0], [autoGenSettings.personalityId, autogenPersonalities] ); const visibleAutoGenHistory = useMemo( () => autoGenHistory.filter((item) => item.mode === autoGenSettings.mode), [autoGenHistory, autoGenSettings.mode] ); const selectedAutogenGeneration = useMemo( () => visibleAutoGenHistory.find((item) => item.generation_id === selectedAutogenGenerationId) ?? visibleAutoGenHistory[0] ?? null, [selectedAutogenGenerationId, visibleAutoGenHistory] ); const visibleAnnotations = useMemo( () => (hideResolvedAnnotations ? annotations.filter((item) => !item.resolved) : annotations), [annotations, hideResolvedAnnotations] ); const selectedAnnotation = visibleAnnotations.find((item) => item.annotation_id === selectedAnnotationId) ?? null; const modalMessage = dialog?.messages.find((item) => item.message_index === commentModal.messageIndex) ?? null; const modalQuestion = useMemo(() => { if (!dialog || commentModal.messageIndex < 0) return null; for (let index = commentModal.messageIndex - 1; index >= 0; index -= 1) { const candidate = dialog.messages[index]; if (candidate?.role === "user") { return candidate; } } return null; }, [commentModal.messageIndex, dialog]); const assistantLiveAnnotationsByMessageId = useMemo(() => { const map = new Map(); for (const item of assistantLiveAnnotations) { if (item.message_id) { map.set(item.message_id, item); } } return map; }, [assistantLiveAnnotations]); const assistantLiveCommentModalMessage = assistantLiveCommentModal.messageIndex >= 0 ? assistantLiveConversation[assistantLiveCommentModal.messageIndex] ?? null : null; const assistantLiveCommentModalQuestion = useMemo(() => { if (assistantLiveCommentModal.messageIndex < 0) return null; for (let index = assistantLiveCommentModal.messageIndex - 1; index >= 0; index -= 1) { const candidate = assistantLiveConversation[index]; if (candidate?.role === "user") { return candidate; } } return null; }, [assistantLiveCommentModal.messageIndex, assistantLiveConversation]); const unifiedVisibleAnnotations = useMemo(() => { const autorunItems: UnifiedCommentListItem[] = visibleAnnotations.map((item) => ({ source: "autorun", key: `autorun:${item.annotation_id}`, updated_at: item.updated_at, rating: item.rating, autorun: item, assistant: null })); const assistantItems: UnifiedCommentListItem[] = assistantLiveAnnotations.map((item) => ({ source: "assistant_live", key: `assistant:${item.annotation_id}`, updated_at: item.updated_at, rating: item.rating, autorun: null, assistant: item })); return [...autorunItems, ...assistantItems].sort((left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at)); }, [assistantLiveAnnotations, visibleAnnotations]); const annotationsAverageRating = useMemo(() => { if (unifiedVisibleAnnotations.length === 0) return null; const avg = unifiedVisibleAnnotations.reduce((acc, item) => acc + item.rating, 0) / unifiedVisibleAnnotations.length; return Number(avg.toFixed(2)); }, [unifiedVisibleAnnotations]); const runSelectItems = useMemo(() => { const list = [...(history?.items ?? [])]; if (activeAsyncJob) { list.unshift(buildLiveRunSummary(activeAsyncJob)); } if (selectedRunId && !list.some((item) => item.run_id === selectedRunId) && runDetail?.run) { list.unshift(runDetail.run); } return list; }, [activeAsyncJob, history?.items, runDetail?.run, selectedRunId]); const log = useCallback( (message: string) => { onLog?.(`[autoruns] ${message}`); }, [onLog] ); const loadAssistantLiveAnnotationsForSession = useCallback( async (sessionIdRaw: string) => { const sessionId = String(sessionIdRaw ?? "").trim(); if (!sessionId) { setAssistantLiveAnnotations([]); return; } try { const payload = await apiClient.loadAssistantAnnotations({ session_id: sessionId, limit: 400 }); setAssistantLiveAnnotations(payload.items ?? []); } catch (error) { const message = error instanceof Error ? error.message : String(error); log(`Assistant live annotations load error: ${message}`); } }, [log] ); const closeAssistantLiveCommentModal = useCallback((options?: { force?: boolean }) => { setAssistantLiveCommentModal((prev) => { if (prev.saving && !options?.force) { return prev; } return { open: false, messageIndex: -1, rating: 3, comment: "", annotationAuthor: "manual_reviewer", saving: false, error: "" }; }); }, []); const closeAssistantLiveSaveModal = useCallback((options?: { force?: boolean }) => { setAssistantLiveSaveModal((prev) => { if (prev.saving && !options?.force) { return prev; } return { open: false, title: "", saving: false, error: "" }; }); }, []); const closeSavedSessionQuestionDeleteModal = useCallback((options?: { force?: boolean }) => { setSavedSessionQuestionDeleteModal((prev) => { if (prev.saving && !options?.force) { return prev; } return { open: false, generationId: "", questionIndex: -1, questionText: "", saving: false, error: "" }; }); }, []); const closeAutoGenDeleteModal = useCallback((options?: { force?: boolean }) => { setAutoGenDeleteModal((prev) => { if (prev.saving && !options?.force) { return prev; } return { open: false, generationId: "", title: "", saving: false, error: "" }; }); }, []); const copyIdentifierToClipboard = useCallback( async (event: React.SyntheticEvent, valueRaw: string, label: string) => { event.stopPropagation(); event.preventDefault(); const value = String(valueRaw ?? "").trim(); if (!value) { return; } try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(value); } else { const textarea = document.createElement("textarea"); textarea.value = value; textarea.setAttribute("readonly", "true"); textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); } log(`${label} copied: ${value}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorText(`Копирование ${label}: ${message}`); log(`copy ${label} error: ${message}`); } }, [log] ); 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([]); setAssistantLiveAnnotations([]); setAssistantLiveInput(""); setAssistantLiveSelectedChip(null); setAssistantLiveStatus(""); setAssistantLiveError(""); closeAssistantLiveCommentModal({ force: true }); log("Live-чат ассистента в истории автопрогонов сброшен."); }, [closeAssistantLiveCommentModal, log]); const sendAssistantLiveMessage = useCallback(async () => { const userMessage = buildAssistantLiveFollowupMessage(assistantLiveInput, assistantLiveSelectedChip); 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); await loadAssistantLiveAnnotationsForSession(response.session_id); 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, assistantLiveSelectedChip, assistantLiveSessionId, assistantLiveUseMock, assistantPromptVersion, connection, loadAssistantLiveAnnotationsForSession, log, prompts ]); const openAssistantLiveSaveModal = useCallback(() => { if (!assistantLiveSessionId.trim() || assistantLiveConversation.length === 0) { setAssistantLiveError("Сначала получите хотя бы один ответ в живой сессии ассистента."); return; } setAssistantLiveError(""); setAssistantLiveSaveModal({ open: true, title: buildSavedSessionDefaultTitle(assistantLiveConversation), saving: false, error: "" }); }, [assistantLiveConversation, assistantLiveSessionId]); const submitAssistantLiveSaveModal = useCallback(async () => { const sessionId = assistantLiveSessionId.trim(); const title = assistantLiveSaveModal.title.trim(); if (!sessionId) { setAssistantLiveSaveModal((prev) => ({ ...prev, error: "Активная сессия ассистента не найдена." })); return; } if (!title) { setAssistantLiveSaveModal((prev) => ({ ...prev, error: "Укажите название сессии." })); return; } setAssistantLiveSaveModal((prev) => ({ ...prev, saving: true, error: "" })); try { const promptFingerprint = [ prompts.systemPrompt, prompts.developerPrompt, prompts.domainPrompt, prompts.schemaNotes, prompts.fewShotExamples ].join("||"); const payload = await apiClient.saveAutoRunAssistantSession({ session_id: sessionId, title, generated_by: autoGenSettings.generatedBy.trim() || undefined, context: { llm_provider: connection.llmProvider, model: connection.model, assistant_prompt_version: assistantPromptVersion, decomposition_prompt_version: decompositionPromptVersion, prompt_fingerprint: promptFingerprint } }); setAutoGenHistory((prev) => [payload.generation, ...prev.filter((item) => item.generation_id !== payload.generation.generation_id)]); setAutoGenSettings((prev) => ({ ...prev, mode: "saved_user_sessions" })); setSelectedAutogenGenerationId(payload.generation.generation_id); closeAssistantLiveSaveModal({ force: true }); log(`Живая сессия сохранена в автопрогоны: ${payload.generation.generation_id}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); setAssistantLiveSaveModal((prev) => ({ ...prev, saving: false, error: message })); log(`Assistant live save error: ${message}`); } }, [ assistantLiveSaveModal.title, assistantLiveSessionId, assistantPromptVersion, autoGenSettings.generatedBy, closeAssistantLiveSaveModal, connection.llmProvider, connection.model, decompositionPromptVersion, log, prompts.developerPrompt, prompts.domainPrompt, prompts.fewShotExamples, prompts.schemaNotes, prompts.systemPrompt ]); const commitLimitInput = useCallback( (raw: string) => { const normalized = raw.trim(); if (!normalized) { setLimitInput(String(filters.limit)); return; } if (!/^\d+$/.test(normalized)) { setLimitInput(String(filters.limit)); return; } const parsed = Number.parseInt(normalized, 10); if (!Number.isFinite(parsed)) { setLimitInput(String(filters.limit)); return; } const next = Math.max(1, Math.min(500, parsed)); if (next !== filters.limit) { setFilters((prev) => ({ ...prev, limit: next })); } setLimitInput(String(next)); }, [filters.limit] ); const commitAutogenCountInput = useCallback( (raw: string) => { const normalized = raw.trim(); if (!normalized) { setAutogenCountInput(String(autoGenSettings.count)); return; } if (!/^\d+$/.test(normalized)) { setAutogenCountInput(String(autoGenSettings.count)); return; } const parsed = Number.parseInt(normalized, 10); if (!Number.isFinite(parsed)) { setAutogenCountInput(String(autoGenSettings.count)); return; } const next = Math.max(1, Math.min(200, parsed)); if (next !== autoGenSettings.count) { setAutoGenSettings((prev) => ({ ...prev, count: next })); } setAutogenCountInput(String(next)); }, [autoGenSettings.count] ); const commitAutogenPromptHeight = useCallback((height: number) => { setAutogenPersonalityPromptHeight(clampAutogenPromptHeight(height)); }, []); const captureAutogenPromptHeight = useCallback( (event: SyntheticEvent) => { const nextHeight = event.currentTarget.offsetHeight; if (Number.isFinite(nextHeight) && nextHeight > 0) { commitAutogenPromptHeight(nextHeight); } }, [commitAutogenPromptHeight] ); const loadAnnotations = useCallback(async () => { setAnnotationsBusy(true); try { const payload = await apiClient.loadAutoRunAnnotations({ limit: 800, manual_case_decision: annotationDecisionFilter }); setAnnotations(payload.items); setManualDecisionSchema(payload.manual_case_decision_schema ?? null); setAvailableManualDecisions(payload.available_manual_case_decisions ?? []); setSelectedAnnotationId((prev) => { if (payload.items.length === 0) return ""; if (payload.items.some((item) => item.annotation_id === prev)) return prev; return payload.items[0].annotation_id; }); } catch (error) { log(`Annotations load error: ${error instanceof Error ? error.message : String(error)}`); } finally { setAnnotationsBusy(false); } }, [annotationDecisionFilter, log]); const loadAutoGenHistory = useCallback(async () => { setAutogenHistoryBusy(true); try { const payload = await apiClient.loadAutoRunAutogenHistory({ limit: 180 }); setAutoGenHistory(payload.items); } catch (error) { log(`Autogen history load error: ${error instanceof Error ? error.message : String(error)}`); } finally { setAutogenHistoryBusy(false); } }, [log]); const loadAutoGenPersonalityCatalog = useCallback(async () => { try { const payload = await apiClient.loadAutoRunAutogenPersonalityCatalog(); const normalized = payload.items .map((item) => ({ id: String(item.id ?? "").trim(), label: String(item.label ?? "").trim(), domain: typeof item.domain === "string" ? item.domain.trim() : "", defaultPrompt: String(item.default_prompt ?? "").trim() })) .filter((item) => item.id.length > 0 && item.label.length > 0); if (normalized.length === 0) { return; } setAutogenPersonalities( normalized.map((item) => ({ id: item.id, label: item.label, domain: item.domain || "", defaultPrompt: item.defaultPrompt || "Генерируй реалистичные вопросы бухгалтера по выбранному профилю. Не выдумывай непокрытые возможности." })) ); } catch (error) { log(`Autogen personality catalog load error: ${error instanceof Error ? error.message : String(error)}`); } }, [log]); const loadPostAnalysis = useCallback(async () => { setPostAnalysisBusy(true); try { const payload = await apiClient.loadAutoRunPostAnalysis({ run_id: selectedRunId && !isLiveRunId(selectedRunId) ? selectedRunId : undefined, limit_per_queue: 30, annotation_limit: 1500, from: localInputToIso(filters.fromLocal), to: localInputToIso(filters.toLocal), target: filters.target, mode: filters.mode, use_mock: filters.useMock, prompt_contains: filters.promptContains.trim() || undefined }); setPostAnalysis(payload); } catch (error) { log(`Post-analysis load error: ${error instanceof Error ? error.message : String(error)}`); setPostAnalysis(null); } finally { setPostAnalysisBusy(false); } }, [filters.fromLocal, filters.mode, filters.promptContains, filters.target, filters.toLocal, filters.useMock, log, selectedRunId]); const generateAutogenBatch = useCallback(async () => { setAutoGenBusy(true); setErrorText(""); try { if (autoGenSettings.mode === "saved_user_sessions") { throw new Error("Пользовательские сессии сохраняются из живого чата, а не генерируются автоматически."); } const activePersonalityPrompt = autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? ""; const promptFingerprint = [ prompts.systemPrompt, prompts.developerPrompt, prompts.domainPrompt, prompts.schemaNotes, prompts.fewShotExamples ] .join("\n") .slice(0, 900); const payload = await apiClient.generateAutoRunQuestions({ mode: autoGenSettings.mode, count: autoGenSettings.count, domain: selectedPersonality.domain || undefined, persist_to_eval_cases: autoGenSettings.persistToEvalCases, generated_by: autoGenSettings.generatedBy.trim() || undefined, llm: { llm_provider: connection.llmProvider, api_key: connection.apiKey, model: connection.model, base_url: connection.baseUrl, temperature: connection.temperature, max_output_tokens: connection.maxOutputTokens }, context: { llm_provider: connection.llmProvider, model: connection.model, assistant_prompt_version: assistantPromptVersion, decomposition_prompt_version: decompositionPromptVersion, prompt_fingerprint: promptFingerprint, autogen_personality_id: selectedPersonality.id, autogen_personality_prompt: activePersonalityPrompt.trim() || undefined } }); log( `Generated ${payload.generation.count} questions (${payload.generation.mode}) id=${payload.generation.generation_id}` + (payload.generation.saved_case_set_file ? ` saved=${payload.generation.saved_case_set_file}` : "") ); setSelectedAutogenGenerationId(payload.generation.generation_id); setEditableGeneratedQuestions([...(payload.generation.questions ?? [])]); await loadAutoGenHistory(); } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorText(`Автогенерация: ${message}`); log(`Autogen generate error: ${message}`); } finally { setAutoGenBusy(false); } }, [ assistantPromptVersion, autoGenSettings.count, autoGenSettings.generatedBy, autoGenSettings.mode, autoGenSettings.personalityId, autoGenSettings.personalityPrompts, autoGenSettings.persistToEvalCases, connection.apiKey, connection.baseUrl, connection.llmProvider, connection.maxOutputTokens, connection.model, connection.temperature, decompositionPromptVersion, loadAutoGenHistory, log, prompts.developerPrompt, prompts.domainPrompt, prompts.fewShotExamples, prompts.schemaNotes, prompts.systemPrompt, selectedPersonality.domain, selectedPersonality.id ]); const loadCaseDialog = useCallback( async (runId: string, caseId: string) => { if (isLiveRunId(runId)) { const liveJobId = jobIdFromLiveRunId(runId); if (activeAsyncJob && activeAsyncJob.job_id === liveJobId) { const live = buildLiveRunDetail(activeAsyncJob, caseId); setSelectedRunId(runId); setSelectedCaseId(live.caseId); setDialog(live.dialog); return; } setDialog(null); return; } 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}`); setDialog(null); log(`Dialog load error for ${runId}/${caseId}: ${message}`); } finally { setDialogBusy(false); } }, [activeAsyncJob, log] ); const loadRunDetail = useCallback( async (runId: string, preferredCaseId?: string) => { if (isLiveRunId(runId)) { const liveJobId = jobIdFromLiveRunId(runId); if (activeAsyncJob && activeAsyncJob.job_id === liveJobId) { const live = buildLiveRunDetail(activeAsyncJob, preferredCaseId ?? ALL_CASES_ID); setSelectedRunId(runId); setSelectedCaseId(live.caseId); setRunDetail(live.detail); setDialog(live.dialog); return; } setSelectedRunId(runId); setSelectedCaseId(""); setRunDetail(null); setDialog(null); return; } setDetailBusy(true); try { const payload = await apiClient.loadAutoRunDetail(runId); setRunDetail(payload); const nextCaseId = (preferredCaseId && (preferredCaseId === ALL_CASES_ID || payload.cases.some((item) => item.case_id === preferredCaseId)) ? preferredCaseId : "") || (payload.cases.length > 0 ? ALL_CASES_ID : "") || ""; setSelectedRunId(runId); setSelectedCaseId(nextCaseId); if (nextCaseId) { await loadCaseDialog(runId, nextCaseId); } else { setDialog(null); } } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorText(`Детализация прогона: ${message}`); setRunDetail(null); setDialog(null); log(`Run detail load error for ${runId}: ${message}`); } finally { setDetailBusy(false); } }, [activeAsyncJob, 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); if (payload.items.length === 0) { 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; await loadRunDetail(nextRunId, keepSelection ? preferredCaseId : undefined); void loadPostAnalysis(); } 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, loadPostAnalysis, loadRunDetail, log ] ); const stopAsyncJobPolling = useCallback(() => { if (asyncJobPollTimerRef.current !== null) { window.clearTimeout(asyncJobPollTimerRef.current); asyncJobPollTimerRef.current = null; } }, []); const pollAsyncJobStatus = useCallback( async (jobId: string) => { try { const payload = await apiClient.loadEvalRunAsyncStatus(jobId); setActiveAsyncJob(payload.job); const liveRunId = toLiveRunId(jobId); if (selectedRunId === liveRunId) { const live = buildLiveRunDetail(payload.job, selectedCaseId || ALL_CASES_ID); setRunDetail(live.detail); setDialog(live.dialog); setSelectedCaseId(live.caseId); } if (payload.job.status === "completed") { stopAsyncJobPolling(); setAutogenRunBusy(false); setAutogenStopBusy(false); setRunningAutogenGenerationId(""); const finalRunId = payload.job.report_summary?.run_id ?? payload.job.run_id; await loadHistory({ keepSelection: true, preferredRunId: finalRunId || selectedRunId, preferredCaseId: ALL_CASES_ID }); await loadAutoGenHistory(); setActiveAsyncJob(null); return; } if (payload.job.status === "failed") { stopAsyncJobPolling(); setAutogenRunBusy(false); setAutogenStopBusy(false); setRunningAutogenGenerationId(""); setErrorText(`Запуск прогонов: ${payload.job.error ?? "неизвестная ошибка"}`); log(`Autogen async run failed: ${payload.job.error ?? "unknown error"}`); return; } if (payload.job.status === "canceled") { stopAsyncJobPolling(); setAutogenRunBusy(false); setAutogenStopBusy(false); setRunningAutogenGenerationId(""); setActiveAsyncJob(null); await loadHistory({ keepSelection: false }); await loadAutoGenHistory(); log(`Autogen async run canceled: job=${payload.job.job_id}`); return; } stopAsyncJobPolling(); asyncJobPollTimerRef.current = window.setTimeout(() => { void pollAsyncJobStatus(jobId); }, 500); } catch (error) { stopAsyncJobPolling(); setAutogenRunBusy(false); setAutogenStopBusy(false); setRunningAutogenGenerationId(""); const message = error instanceof Error ? error.message : String(error); setErrorText(`Запуск прогонов: ${message}`); log(`Autogen async status error: ${message}`); } }, [loadAutoGenHistory, loadHistory, log, selectedCaseId, selectedRunId, stopAsyncJobPolling] ); const runAutogenCampaign = useCallback(async (generationOverride?: AutoGenHistoryRecord, questionsOverride?: string[]) => { stopAsyncJobPolling(); setAutogenRunBusy(true); setErrorText(""); try { const generation = generationOverride ?? selectedAutogenGeneration; if (!generation) { throw new Error("История автогенерации пуста. Сначала сгенерируйте пачку вопросов."); } const sourceQuestions = questionsOverride ?? (selectedAutogenGeneration?.generation_id === generation.generation_id ? editableGeneratedQuestions : generation.questions); const questionsForRun = sourceQuestions .map((item) => item.trim()) .filter((item) => item.length > 0); if (questionsForRun.length === 0) { throw new Error("Нет вопросов для запуска: список пустой после ручного редактирования."); } const useMockForRun = filters.useMock === "true"; const effectiveAnalysisDate = normalizeAnalysisDateInput(analysisDate); const useScenarioReplay = generation.mode === "saved_user_sessions"; const payload = await apiClient.startEvalRunAsync({ connection, prompts, promptVersion: assistantPromptVersion, mode: "single-pass-strict", caseSetFile: useScenarioReplay ? undefined : generation.saved_case_set_file ?? undefined, useMock: useMockForRun, evalTarget: "assistant_stage1", questions: useScenarioReplay ? undefined : questionsForRun, scenarioQuestions: useScenarioReplay ? questionsForRun : undefined, scenarioTitle: useScenarioReplay ? generation.title ?? undefined : undefined, analysisDate: useScenarioReplay ? undefined : effectiveAnalysisDate || undefined }); const liveJob = payload.job; setRunningAutogenGenerationId(generation.generation_id); setAutogenStopBusy(false); setActiveAsyncJob(liveJob); const liveRunId = toLiveRunId(liveJob.job_id); const live = buildLiveRunDetail(liveJob, ALL_CASES_ID); setSelectedRunId(liveRunId); setSelectedCaseId(live.caseId); setRunDetail(live.detail); setDialog(live.dialog); 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}` : "") + (useScenarioReplay ? ", replay_mode=saved_user_session_scenario" : effectiveAnalysisDate ? `, analysis_date=${effectiveAnalysisDate}` : ", analysis_date=current_state") ); void pollAsyncJobStatus(liveJob.job_id); } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorText(`Запуск прогонов: ${message}`); log(`Autogen run error: ${message}`); setAutogenRunBusy(false); setAutogenStopBusy(false); setRunningAutogenGenerationId(""); } }, [ analysisDate, assistantPromptVersion, connection, editableGeneratedQuestions, filters.useMock, log, pollAsyncJobStatus, prompts, selectedAutogenGeneration, stopAsyncJobPolling ]); const stopAutogenCampaign = useCallback(async () => { const jobId = activeAsyncJob?.job_id ?? ""; if (!jobId) { setAutogenRunBusy(false); setAutogenStopBusy(false); setRunningAutogenGenerationId(""); setActiveAsyncJob(null); stopAsyncJobPolling(); return; } setAutogenStopBusy(true); setErrorText(""); try { const payload = await apiClient.cancelEvalRunAsync(jobId); stopAsyncJobPolling(); setActiveAsyncJob(null); setAutogenRunBusy(false); setAutogenStopBusy(false); setRunningAutogenGenerationId(""); await loadHistory({ keepSelection: false }); await loadAutoGenHistory(); log(`Autogen async run stopped: job=${payload.job.job_id}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); setAutogenStopBusy(false); setErrorText(`Остановка прогона: ${message}`); log(`Autogen stop error: ${message}`); } }, [activeAsyncJob, loadAutoGenHistory, loadHistory, log, stopAsyncJobPolling]); const openCommentModal = useCallback((message: AutoRunDialogMessage) => { if (message.role !== "assistant") return; const resolvedCaseId = message.case_id ?? selectedCaseId; const resolvedCaseMessageIndex = message.case_message_index ?? message.message_index; setCommentModal({ open: true, caseId: resolvedCaseId, caseMessageIndex: resolvedCaseMessageIndex, messageIndex: message.message_index, rating: message.annotation?.rating ?? 3, comment: message.annotation?.comment ?? "", manualCaseDecision: message.annotation?.manual_case_decision ?? DEFAULT_MANUAL_DECISION, annotationAuthor: message.annotation?.annotation_author ?? autoGenSettings.generatedBy, saving: false, error: "" }); }, [autoGenSettings.generatedBy, selectedCaseId]); const closeCommentModal = useCallback((options?: { force?: boolean }) => { setCommentModal((prev) => { if (prev.saving && !options?.force) return prev; return { open: false, caseId: "", caseMessageIndex: -1, messageIndex: -1, rating: 3, comment: "", manualCaseDecision: DEFAULT_MANUAL_DECISION, annotationAuthor: autoGenSettings.generatedBy, saving: false, error: "" }; }); }, [autoGenSettings.generatedBy]); const submitCommentModal = useCallback(async () => { const targetRunId = selectedRunId; const targetCaseId = commentModal.caseId; const targetCaseMessageIndex = commentModal.caseMessageIndex; if (!targetRunId || !targetCaseId || targetCaseMessageIndex < 0) return; if (isLiveRunId(targetRunId)) { setCommentModal((prev) => ({ ...prev, error: "Комментарий можно сохранить после завершения прогона." })); return; } if (!commentModal.comment.trim()) { setCommentModal((prev) => ({ ...prev, error: "Добавьте комментарий." })); return; } setCommentModal((prev) => ({ ...prev, saving: true, error: "" })); try { await apiClient.saveAutoRunAnnotation({ run_id: targetRunId, case_id: targetCaseId, message_index: targetCaseMessageIndex, rating: commentModal.rating, comment: commentModal.comment.trim(), manual_case_decision: commentModal.manualCaseDecision, annotation_author: commentModal.annotationAuthor.trim() || undefined }); closeCommentModal({ force: true }); void Promise.all([loadRunDetail(targetRunId, selectedCaseId), loadAnnotations(), loadPostAnalysis()]).catch((error) => { const message = error instanceof Error ? error.message : String(error); setErrorText(`Обновление после комментария: ${message}`); log(`Comment refresh error: ${message}`); }); } catch (error) { setCommentModal((prev) => ({ ...prev, saving: false, error: error instanceof Error ? error.message : String(error) })); } }, [ closeCommentModal, commentModal.annotationAuthor, commentModal.caseId, commentModal.caseMessageIndex, commentModal.comment, commentModal.manualCaseDecision, commentModal.rating, loadAnnotations, loadPostAnalysis, loadRunDetail, log, selectedCaseId, selectedRunId ]); const canCommentAssistantLiveMessage = useCallback((item: AssistantConversationItem): boolean => item.role === "assistant", []); const isAssistantLiveMessageCommented = useCallback( (item: AssistantConversationItem): boolean => item.role === "assistant" && assistantLiveAnnotationsByMessageId.has(item.message_id), [assistantLiveAnnotationsByMessageId] ); const openAssistantLiveCommentModal = useCallback( (item: AssistantConversationItem, index: number) => { if (item.role !== "assistant") { return; } const sessionIdFromState = assistantLiveSessionId.trim(); const sessionIdFromItem = String(item.session_id ?? "").trim(); const resolvedSessionId = sessionIdFromState || sessionIdFromItem; if (!resolvedSessionId) { setAssistantLiveError("Сначала получите ответ ассистента в активной сессии."); return; } if (!sessionIdFromState && sessionIdFromItem) { setAssistantLiveSessionId(sessionIdFromItem); } const existing = assistantLiveAnnotationsByMessageId.get(item.message_id) ?? null; setAssistantLiveError(""); setAssistantLiveCommentModal({ open: true, messageIndex: index, rating: existing?.rating ?? 3, comment: existing?.comment ?? "", annotationAuthor: existing?.annotation_author ?? "manual_reviewer", saving: false, error: "" }); }, [assistantLiveAnnotationsByMessageId, assistantLiveSessionId] ); const submitAssistantLiveCommentModal = useCallback(async () => { if (assistantLiveCommentModal.messageIndex < 0) { return; } if (!assistantLiveCommentModal.comment.trim()) { setAssistantLiveCommentModal((prev) => ({ ...prev, error: "Добавьте комментарий." })); return; } const modalMessage = assistantLiveConversation[assistantLiveCommentModal.messageIndex] ?? null; const sessionId = assistantLiveSessionId.trim() || (modalMessage?.role === "assistant" ? String(modalMessage.session_id ?? "").trim() : ""); if (!sessionId) { setAssistantLiveCommentModal((prev) => ({ ...prev, error: "Сессия ассистента не найдена." })); return; } setAssistantLiveCommentModal((prev) => ({ ...prev, saving: true, error: "" })); try { const payload = await apiClient.saveAssistantAnnotation({ session_id: sessionId, message_index: assistantLiveCommentModal.messageIndex, rating: assistantLiveCommentModal.rating, comment: assistantLiveCommentModal.comment.trim(), annotation_author: assistantLiveCommentModal.annotationAuthor.trim() || undefined }); setAssistantLiveAnnotations((prev) => { const next = [...prev]; const index = next.findIndex((item) => item.annotation_id === payload.annotation.annotation_id); if (index >= 0) { next[index] = payload.annotation; } else { next.unshift(payload.annotation); } return next.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at)); }); closeAssistantLiveCommentModal({ force: true }); } catch (error) { const message = error instanceof Error ? error.message : String(error); setAssistantLiveCommentModal((prev) => ({ ...prev, saving: false, error: message })); } }, [ assistantLiveCommentModal.annotationAuthor, assistantLiveCommentModal.comment, assistantLiveCommentModal.messageIndex, assistantLiveCommentModal.rating, assistantLiveConversation, assistantLiveSessionId, closeAssistantLiveCommentModal ]); const requestDeleteSavedSessionQuestion = useCallback( (questionIndex: number) => { if (!selectedAutogenGeneration || selectedAutogenGeneration.mode !== "saved_user_sessions") { return; } const questionText = editableGeneratedQuestions[questionIndex] ?? ""; setSavedSessionQuestionDeleteModal({ open: true, generationId: selectedAutogenGeneration.generation_id, questionIndex, questionText, saving: false, error: "" }); }, [editableGeneratedQuestions, selectedAutogenGeneration] ); const submitSavedSessionQuestionDelete = useCallback(async () => { const generationId = savedSessionQuestionDeleteModal.generationId; const questionIndex = savedSessionQuestionDeleteModal.questionIndex; if (!generationId || questionIndex < 0) { return; } const nextQuestions = editableGeneratedQuestions.filter((_, index) => index !== questionIndex); if (nextQuestions.length === 0) { setSavedSessionQuestionDeleteModal((prev) => ({ ...prev, error: "Нельзя удалить последний вопрос из сохраненной сессии." })); return; } setSavedSessionQuestionDeleteModal((prev) => ({ ...prev, saving: true, error: "" })); try { const payload = await apiClient.updateAutoRunAutogenQuestions({ generation_id: generationId, questions: nextQuestions }); setAutoGenHistory((prev) => prev.map((item) => (item.generation_id === generationId ? payload.generation : item)) ); setEditableGeneratedQuestions(payload.generation.questions); closeSavedSessionQuestionDeleteModal({ force: true }); log(`Обновлена сохраненная сессия: ${generationId}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); setSavedSessionQuestionDeleteModal((prev) => ({ ...prev, saving: false, error: message })); log(`Saved session question delete error: ${message}`); } }, [ closeSavedSessionQuestionDeleteModal, editableGeneratedQuestions, log, savedSessionQuestionDeleteModal.generationId, savedSessionQuestionDeleteModal.questionIndex ]); const updateGeneratedQuestions = useCallback( async (nextQuestions: string[], options?: { successLog?: string; revertQuestions?: string[] }) => { const generationId = selectedAutogenGeneration?.generation_id ?? ""; const revertQuestions = options?.revertQuestions ?? editableGeneratedQuestions; setEditableGeneratedQuestions(nextQuestions); if (!generationId) { return true; } setGeneratedQuestionsBusy(true); try { const payload = await apiClient.updateAutoRunAutogenQuestions({ generation_id: generationId, questions: nextQuestions }); setAutoGenHistory((prev) => prev.map((item) => (item.generation_id === generationId ? payload.generation : item)) ); setEditableGeneratedQuestions([...(payload.generation.questions ?? [])]); if (options?.successLog) { log(options.successLog); } return true; } catch (error) { const message = error instanceof Error ? error.message : String(error); setEditableGeneratedQuestions(revertQuestions); setErrorText(`Вопросы к запуску: ${message}`); log(`Autogen questions update error: ${message}`); return false; } finally { setGeneratedQuestionsBusy(false); } }, [editableGeneratedQuestions, log, selectedAutogenGeneration] ); const startQuestionEdit = useCallback( (questionIndex: number) => { setEditingQuestionIndex(questionIndex); setEditingQuestionDraft(editableGeneratedQuestions[questionIndex] ?? ""); }, [editableGeneratedQuestions] ); const stopQuestionEdit = useCallback(() => { setEditingQuestionIndex(null); setEditingQuestionDraft(""); }, []); const commitQuestionEdit = useCallback( async (questionIndex: number | null) => { if (questionIndex === null) { return; } const currentQuestion = editableGeneratedQuestions[questionIndex] ?? ""; const nextText = editingQuestionDraft.trim(); if (!nextText || nextText === currentQuestion) { stopQuestionEdit(); return; } const nextQuestions = editableGeneratedQuestions.map((item, index) => (index === questionIndex ? nextText : item)); const saved = await updateGeneratedQuestions(nextQuestions, { successLog: `Список вопросов обновлен: ${selectedAutogenGeneration?.generation_id ?? "local"}`, revertQuestions: editableGeneratedQuestions }); if (saved) { stopQuestionEdit(); } }, [editableGeneratedQuestions, editingQuestionDraft, selectedAutogenGeneration, stopQuestionEdit, updateGeneratedQuestions] ); const handleQuestionEditorBlur = useCallback(() => { void commitQuestionEdit(editingQuestionIndex); }, [commitQuestionEdit, editingQuestionIndex]); const handleQuestionEditorKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); void commitQuestionEdit(editingQuestionIndex); return; } if (event.key === "Escape") { event.preventDefault(); stopQuestionEdit(); } }, [commitQuestionEdit, editingQuestionIndex, stopQuestionEdit] ); const handleAddGeneratedQuestion = useCallback(async () => { const nextQuestions = [...editableGeneratedQuestions, "Новый вопрос"]; const nextIndex = nextQuestions.length - 1; const saved = await updateGeneratedQuestions(nextQuestions, { successLog: `В список добавлен вопрос: ${selectedAutogenGeneration?.generation_id ?? "local"}`, revertQuestions: editableGeneratedQuestions }); if (saved) { setEditingQuestionIndex(nextIndex); setEditingQuestionDraft(nextQuestions[nextIndex]); } }, [editableGeneratedQuestions, selectedAutogenGeneration, updateGeneratedQuestions]); const handleDeleteGeneratedQuestion = useCallback( async (questionIndex: number) => { if (editableGeneratedQuestions.length <= 1) { setErrorText("В списке должен остаться хотя бы один вопрос."); return; } const nextQuestions = editableGeneratedQuestions.filter((_, index) => index !== questionIndex); const saved = await updateGeneratedQuestions(nextQuestions, { successLog: `Из списка удален вопрос: ${selectedAutogenGeneration?.generation_id ?? "local"}`, revertQuestions: editableGeneratedQuestions }); if (!saved) { return; } setEditingQuestionIndex((prev) => { if (prev === null) return prev; if (prev === questionIndex) return null; if (prev > questionIndex) return prev - 1; return prev; }); setEditingQuestionDraft(""); }, [editableGeneratedQuestions, selectedAutogenGeneration, updateGeneratedQuestions] ); const handleQuestionDragStart = useCallback( (event: DragEvent, questionIndex: number) => { if (generatedQuestionsBusy) { event.preventDefault(); return; } setDraggingQuestionIndex(questionIndex); setDragOverQuestionIndex(questionIndex); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", String(questionIndex)); }, [generatedQuestionsBusy] ); const handleQuestionDragOver = useCallback( (event: DragEvent, questionIndex: number) => { event.preventDefault(); if (dragOverQuestionIndex !== questionIndex) { setDragOverQuestionIndex(questionIndex); } event.dataTransfer.dropEffect = "move"; }, [dragOverQuestionIndex] ); const handleQuestionDrop = useCallback( async (event: DragEvent, questionIndex: number) => { event.preventDefault(); const fromIndex = draggingQuestionIndex; setDragOverQuestionIndex(null); setDraggingQuestionIndex(null); if (fromIndex === null || fromIndex === questionIndex) { return; } const nextQuestions = [...editableGeneratedQuestions]; const [movedQuestion] = nextQuestions.splice(fromIndex, 1); nextQuestions.splice(questionIndex, 0, movedQuestion); await updateGeneratedQuestions(nextQuestions, { successLog: `Порядок вопросов обновлен: ${selectedAutogenGeneration?.generation_id ?? "local"}`, revertQuestions: editableGeneratedQuestions }); }, [draggingQuestionIndex, editableGeneratedQuestions, selectedAutogenGeneration, updateGeneratedQuestions] ); const handleQuestionDragEnd = useCallback(() => { setDraggingQuestionIndex(null); setDragOverQuestionIndex(null); }, []); const toggleSavedSessionQuestions = useCallback((generationId: string) => { setSelectedAutogenGenerationId(generationId); setExpandedSavedSessionGenerationId((prev) => (prev === generationId ? "" : generationId)); }, []); const openAutoGenDeleteModal = useCallback((item: AutoGenHistoryRecord) => { setAutoGenDeleteModal({ open: true, generationId: item.generation_id, title: item.title ?? `${formatAutoGenModeLabel(item.mode)} ${formatDateTime(item.created_at)}`, saving: false, error: "" }); }, []); const submitAutoGenDeleteModal = useCallback(async () => { const generationId = autoGenDeleteModal.generationId.trim(); if (!generationId) { return; } setAutoGenDeleteModal((prev) => ({ ...prev, saving: true, error: "" })); try { const payload = await apiClient.deleteAutoRunAutogenHistoryRecord(generationId); setAutoGenHistory((prev) => prev.filter((item) => item.generation_id !== payload.generation_id)); closeAutoGenDeleteModal({ force: true }); log( `Удален набор автопрогона: ${payload.generation_id}` + (payload.deleted_files.length > 0 ? `, files=${payload.deleted_files.length}` : "") ); } catch (error) { const message = error instanceof Error ? error.message : String(error); setAutoGenDeleteModal((prev) => ({ ...prev, saving: false, error: message })); log(`Autogen record delete error: ${message}`); } }, [autoGenDeleteModal.generationId, closeAutoGenDeleteModal, log]); const applyLocalAnnotationPatch = useCallback((annotation: AutoRunAnnotationRecord) => { setAnnotations((prev) => prev.map((item) => item.annotation_id === annotation.annotation_id ? { ...item, ...annotation } : item ) ); setDialog((prev) => { if (!prev) return prev; return { ...prev, annotations: prev.annotations.map((item) => (item.annotation_id === annotation.annotation_id ? annotation : item)), messages: prev.messages.map((item) => { if (!item.annotation || item.annotation.annotation_id !== annotation.annotation_id) { return item; } return { ...item, commented: true, annotation }; }) }; }); }, []); const toggleAnnotationResolved = useCallback( async (annotation: AutoRunAnnotationRecord, nextResolved: boolean) => { if (!annotation.annotation_id) return; if (isLiveRunId(annotation.run_id)) { setErrorText("\u0421\u0442\u0430\u0442\u0443\u0441 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u043c\u0435\u043d\u044f\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043d\u043d\u044b\u0445 \u043f\u0440\u043e\u0433\u043e\u043d\u043e\u0432."); return; } setAnnotationResolutionBusyId(annotation.annotation_id); try { const payload = await apiClient.updateAutoRunAnnotation({ annotation_id: annotation.annotation_id, resolved: nextResolved, resolved_by: autoGenSettings.generatedBy || undefined }); applyLocalAnnotationPatch(payload.annotation); void loadPostAnalysis(); } catch (error) { const message = error instanceof Error ? error.message : String(error); setErrorText(`\u0421\u043c\u0435\u043d\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u043a\u0435\u0439\u0441\u0430: ${message}`); log(`Annotation resolve toggle error: ${message}`); } finally { setAnnotationResolutionBusyId(""); } }, [applyLocalAnnotationPatch, autoGenSettings.generatedBy, loadPostAnalysis, log] ); const openAnnotationContext = useCallback( async (annotation: AutoRunAnnotationRecord) => { setSelectedAnnotationId(annotation.annotation_id); await loadRunDetail(annotation.run_id, annotation.case_id); if (!history?.items.some((item) => item.run_id === annotation.run_id)) { setErrorText("Комментарий относится к прогону вне текущего фильтра. Детали загружены напрямую."); } }, [history?.items, loadRunDetail] ); useEffect(() => { if (initialLoadDoneRef.current) return; initialLoadDoneRef.current = true; void loadHistory({ keepSelection: false }); void loadAutoGenHistory(); void loadAutoGenPersonalityCatalog(); void loadPostAnalysis(); }, [loadAutoGenHistory, loadAutoGenPersonalityCatalog, loadHistory, loadPostAnalysis]); useEffect(() => { if (!initialLoadDoneRef.current) return; void loadAnnotations(); }, [annotationDecisionFilter, loadAnnotations]); useEffect(() => { setSelectedAnnotationId((prev) => { if (visibleAnnotations.length === 0) return ""; if (visibleAnnotations.some((item) => item.annotation_id === prev)) return prev; return visibleAnnotations[0].annotation_id; }); }, [visibleAnnotations]); useEffect(() => { setSelectedAutogenGenerationId((prev) => { if (visibleAutoGenHistory.length === 0) return ""; if (prev && visibleAutoGenHistory.some((item) => item.generation_id === prev)) return prev; return visibleAutoGenHistory[0].generation_id; }); }, [visibleAutoGenHistory]); useEffect(() => { if (!selectedAutogenGeneration) { setEditableGeneratedQuestions([]); stopQuestionEdit(); setDraggingQuestionIndex(null); setDragOverQuestionIndex(null); return; } setEditableGeneratedQuestions([...selectedAutogenGeneration.questions]); stopQuestionEdit(); setDraggingQuestionIndex(null); setDragOverQuestionIndex(null); }, [selectedAutogenGeneration, stopQuestionEdit]); useEffect(() => { if (editingQuestionIndex === null) { return; } const timer = window.setTimeout(() => { questionEditorRef.current?.focus(); questionEditorRef.current?.select(); }, 0); return () => window.clearTimeout(timer); }, [editingQuestionIndex]); useEffect(() => { if (!isSavedUserSessionsMode) { setExpandedSavedSessionGenerationId(""); return; } if ( expandedSavedSessionGenerationId && !visibleAutoGenHistory.some((item) => item.generation_id === expandedSavedSessionGenerationId) ) { setExpandedSavedSessionGenerationId(""); } }, [expandedSavedSessionGenerationId, isSavedUserSessionsMode, visibleAutoGenHistory]); useEffect(() => { setLimitInput(String(filters.limit)); }, [filters.limit]); useEffect(() => { setAutogenCountInput(String(autoGenSettings.count)); }, [autoGenSettings.count]); useEffect(() => { if (!assistantLiveSessionId.trim()) { setAssistantLiveAnnotations([]); return; } void loadAssistantLiveAnnotationsForSession(assistantLiveSessionId); }, [assistantLiveSessionId, loadAssistantLiveAnnotationsForSession]); useEffect(() => { if (!activeAsyncJob) return; const liveRunId = toLiveRunId(activeAsyncJob.job_id); if (selectedRunId !== liveRunId) return; const live = buildLiveRunDetail(activeAsyncJob, selectedCaseId || ALL_CASES_ID); setRunDetail(live.detail); setDialog(live.dialog); setSelectedCaseId(live.caseId); }, [activeAsyncJob, selectedCaseId, selectedRunId]); useEffect(() => { return () => { stopAsyncJobPolling(); }; }, [stopAsyncJobPolling]); useEffect(() => { if (autogenPersonalities.length === 0) return; setAutoGenSettings((prev) => { let changed = false; const nextPrompts: Record = { ...prev.personalityPrompts }; for (const item of autogenPersonalities) { if (typeof nextPrompts[item.id] !== "string" || nextPrompts[item.id].trim().length === 0) { nextPrompts[item.id] = item.defaultPrompt; changed = true; } } let nextPersonalityId = prev.personalityId; if (!autogenPersonalities.some((item) => item.id === prev.personalityId)) { nextPersonalityId = autogenPersonalities[0].id; changed = true; } if (!changed) return prev; return { ...prev, personalityId: nextPersonalityId, personalityPrompts: nextPrompts }; }); }, [autogenPersonalities]); useEffect(() => { const raw = localStorage.getItem(AUTORUNS_UI_CONFIG_KEY); if (!raw) return; try { const parsed = JSON.parse(raw) as AutoRunsUiConfig; if (parsed.filters) { const savedFilters = parsed.filters; setFilters((prev) => ({ ...prev, ...savedFilters, limit: typeof savedFilters.limit === "number" ? Math.max(1, Math.min(500, savedFilters.limit)) : prev.limit })); } if (typeof parsed.analysisDate === "string") { setAnalysisDate(normalizeAnalysisDateInput(parsed.analysisDate)); } if (typeof parsed.autogenPersonalityPromptHeight === "number") { setAutogenPersonalityPromptHeight(clampAutogenPromptHeight(parsed.autogenPersonalityPromptHeight)); } if (parsed.groupsExpanded) { if (typeof parsed.groupsExpanded.filters === "boolean") { setFiltersGroupExpanded(parsed.groupsExpanded.filters); } if (typeof parsed.groupsExpanded.generationContext === "boolean") { setGenerationContextGroupExpanded(parsed.groupsExpanded.generationContext); } if (typeof parsed.groupsExpanded.autogen === "boolean") { setAutogenGroupExpanded(parsed.groupsExpanded.autogen); } if (typeof parsed.groupsExpanded.savedSessions === "boolean") { setSavedSessionsGroupExpanded(parsed.groupsExpanded.savedSessions); } } if (parsed.autoGenSettings) { setAutoGenSettings((prev) => { const nextPrompts: Record = { ...prev.personalityPrompts }; const incomingPrompts = parsed.autoGenSettings?.personalityPrompts ?? {}; for (const [key, value] of Object.entries(incomingPrompts)) { if (typeof value === "string" && key.trim().length > 0) { nextPrompts[key.trim()] = value; } } const nextPersonalityId = typeof parsed.autoGenSettings?.personalityId === "string" && parsed.autoGenSettings.personalityId.trim().length > 0 ? parsed.autoGenSettings.personalityId.trim() : prev.personalityId; return { ...prev, mode: parsed.autoGenSettings?.mode === "codex_creative" || parsed.autoGenSettings?.mode === "qwen_seed" || parsed.autoGenSettings?.mode === "saved_user_sessions" ? parsed.autoGenSettings.mode : prev.mode, count: typeof parsed.autoGenSettings?.count === "number" ? Math.max(1, Math.min(200, parsed.autoGenSettings.count)) : prev.count, personalityId: nextPersonalityId, personalityPrompts: nextPrompts, persistToEvalCases: typeof parsed.autoGenSettings?.persistToEvalCases === "boolean" ? parsed.autoGenSettings.persistToEvalCases : prev.persistToEvalCases, generatedBy: typeof parsed.autoGenSettings?.generatedBy === "string" ? parsed.autoGenSettings.generatedBy : prev.generatedBy }; }); } if ( parsed.annotationDecisionFilter === "all" || (typeof parsed.annotationDecisionFilter === "string" && parsed.annotationDecisionFilter.length > 0) ) { setAnnotationDecisionFilter(parsed.annotationDecisionFilter as ManualCaseDecision | "all"); } if (typeof parsed.hideResolvedAnnotations === "boolean") { setHideResolvedAnnotations(parsed.hideResolvedAnnotations); } } catch { // ignore broken local cache } }, []); const saveUiConfig = useCallback(() => { const payload: AutoRunsUiConfig = { filters, analysisDate, autogenPersonalityPromptHeight, groupsExpanded: { filters: filtersGroupExpanded, generationContext: generationContextGroupExpanded, autogen: autogenGroupExpanded, savedSessions: savedSessionsGroupExpanded }, autoGenSettings: { mode: autoGenSettings.mode, count: autoGenSettings.count, personalityId: autoGenSettings.personalityId, personalityPrompts: autoGenSettings.personalityPrompts, persistToEvalCases: autoGenSettings.persistToEvalCases, generatedBy: autoGenSettings.generatedBy }, annotationDecisionFilter, hideResolvedAnnotations }; localStorage.setItem(AUTORUNS_UI_CONFIG_KEY, JSON.stringify(payload)); }, [ analysisDate, annotationDecisionFilter, autoGenSettings, autogenGroupExpanded, autogenPersonalityPromptHeight, filters, filtersGroupExpanded, generationContextGroupExpanded, hideResolvedAnnotations, savedSessionsGroupExpanded ]); useEffect(() => { const onSave = () => { saveUiConfig(); log("Сохранены настройки панели автопрогонов."); }; window.addEventListener(AUTORUNS_SAVE_EVENT, onSave as EventListener); return () => { window.removeEventListener(AUTORUNS_SAVE_EVENT, onSave as EventListener); }; }, [log, saveUiConfig]); return (
{showSettingsMode ? (

Настройки

) : null} {showAutoRunsMode ? (

Автопрогоны

Настройки выборки

{filtersGroupExpanded ? ( <>
{(history?.available.prompt_versions ?? []).map((item) => (
) : null}

Контур генерации

{generationContextGroupExpanded ? (
Провайдер: {connection.llmProvider}
Модель: {connection.model || "нет данных"}
Промпт ассистента: {assistantPromptVersion}
Промпт декомпозиции: {decompositionPromptVersion}
) : null}

Автопрогоны

{autogenGroupExpanded ? ( <>
{!isSavedUserSessionsMode ? ( <>