ЮИ - Добавить удаление сохраненных наборов автопрогонов с удалением файлов на бэке

This commit is contained in:
2026-04-16 21:02:30 +03:00
parent f3255cb3b8
commit a3a61b3a0f
22 changed files with 1895 additions and 1996 deletions
@@ -16,9 +16,13 @@ interface AssistantPanelProps {
onUseMockChange: (value: boolean) => void;
onSend: () => Promise<void> | void;
onClear: () => void;
onSaveSession?: () => void;
busy: boolean;
saveBusy?: boolean;
saveDisabled?: boolean;
statusText: string;
errorMessage: string;
showSaveAction?: boolean;
showCommentAction?: boolean;
onCommentAssistantMessage?: (item: AssistantConversationItem, index: number) => void;
isAssistantMessageCommented?: (item: AssistantConversationItem, index: number) => boolean;
@@ -295,9 +299,13 @@ export function AssistantPanel({
onUseMockChange,
onSend,
onClear,
onSaveSession,
busy,
saveBusy = false,
saveDisabled = false,
statusText,
errorMessage,
showSaveAction = false,
showCommentAction = false,
onCommentAssistantMessage,
isAssistantMessageCommented,
@@ -383,6 +391,16 @@ export function AssistantPanel({
>
Скопировать техчат
</button>
{showSaveAction ? (
<button
type="button"
className="assistant-copy-btn"
onClick={() => onSaveSession?.()}
disabled={saveBusy || saveDisabled}
>
{saveBusy ? "Сохраняю..." : "Сохранить"}
</button>
) : null}
<button type="button" className="assistant-copy-btn" onClick={() => onClear()} disabled={busy && conversation.length === 0}>
Сбросить сессию
</button>
@@ -63,7 +63,6 @@ interface AutoRunsHistoryPanelProps {
showSettingsMode: boolean;
showAutoRunsMode: boolean;
showAssistantMode: boolean;
showDecompositionMode: boolean;
showProgressMode: boolean;
showCommentsMode: boolean;
onLog?: (message: string) => void;
@@ -112,6 +111,30 @@ interface AssistantLiveCommentModalState {
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;
@@ -259,6 +282,19 @@ function formatDateTime(iso: string | null): string {
return new Date(parsed).toLocaleString("ru-RU");
}
function formatAutoGenModeLabel(mode: AutoGenMode): string {
if (mode === "saved_user_sessions") {
return "Пользовательские сессии";
}
return mode;
}
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))));
@@ -282,10 +318,6 @@ function trendLabel(value: "up" | "down" | "flat"): string {
return "Без изменений";
}
function getSelectedCase(cases: AutoRunCaseSummary[], caseId: string): AutoRunCaseSummary | null {
return cases.find((item) => item.case_id === caseId) ?? null;
}
function renderRatingDots(rating: number): string {
const safe = Math.max(1, Math.min(5, Math.round(rating)));
return `${"●".repeat(safe)}${"○".repeat(5 - safe)}`;
@@ -527,7 +559,6 @@ export function AutoRunsHistoryPanel({
showSettingsMode,
showAutoRunsMode,
showAssistantMode,
showDecompositionMode,
showProgressMode,
showCommentsMode,
onLog
@@ -598,19 +629,44 @@ export function AutoRunsHistoryPanel({
saving: false,
error: ""
});
const [assistantLiveSaveModal, setAssistantLiveSaveModal] = useState<AssistantLiveSaveModalState>({
open: false,
title: "",
saving: false,
error: ""
});
const [savedSessionQuestionDeleteModal, setSavedSessionQuestionDeleteModal] = useState<SavedSessionQuestionDeleteModalState>({
open: false,
generationId: "",
questionIndex: -1,
questionText: "",
saving: false,
error: ""
});
const [autoGenDeleteModal, setAutoGenDeleteModal] = useState<AutoGenDeleteModalState>({
open: false,
generationId: "",
title: "",
saving: false,
error: ""
});
const initialLoadDoneRef = useRef(false);
const asyncJobPollTimerRef = useRef<number | null>(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(
() => autoGenHistory.find((item) => item.generation_id === selectedAutogenGenerationId) ?? autoGenHistory[0] ?? null,
[autoGenHistory, selectedAutogenGenerationId]
() => visibleAutoGenHistory.find((item) => item.generation_id === selectedAutogenGenerationId) ?? visibleAutoGenHistory[0] ?? null,
[selectedAutogenGenerationId, visibleAutoGenHistory]
);
const activeCase = runDetail ? getSelectedCase(runDetail.cases, selectedCaseId) : null;
const visibleAnnotations = useMemo(
() => (hideResolvedAnnotations ? annotations.filter((item) => !item.resolved) : annotations),
[annotations, hideResolvedAnnotations]
@@ -731,11 +787,56 @@ export function AutoRunsHistoryPanel({
});
}, []);
const copyRunIdToClipboard = useCallback(
async (event: React.SyntheticEvent, runId: string) => {
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(runId ?? "").trim();
const value = String(valueRaw ?? "").trim();
if (!value) {
return;
}
@@ -753,11 +854,11 @@ export function AutoRunsHistoryPanel({
document.execCommand("copy");
document.body.removeChild(textarea);
}
log(`run id copied: ${value}`);
log(`${label} copied: ${value}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setErrorText(`Копирование run id: ${message}`);
log(`copy run id error: ${message}`);
setErrorText(`Копирование ${label}: ${message}`);
log(`copy ${label} error: ${message}`);
}
},
[log]
@@ -844,6 +945,80 @@ export function AutoRunsHistoryPanel({
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();
@@ -1000,6 +1175,9 @@ export function AutoRunsHistoryPanel({
setAutoGenBusy(true);
setErrorText("");
try {
if (autoGenSettings.mode === "saved_user_sessions") {
throw new Error("Пользовательские сессии сохраняются из живого чата, а не генерируются автоматически.");
}
const activePersonalityPrompt = autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? "";
const promptFingerprint = [
prompts.systemPrompt,
@@ -1283,16 +1461,19 @@ export function AutoRunsHistoryPanel({
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: generation.saved_case_set_file ?? undefined,
caseSetFile: useScenarioReplay ? undefined : generation.saved_case_set_file ?? undefined,
useMock: useMockForRun,
evalTarget: "assistant_stage1",
questions: questionsForRun,
analysisDate: effectiveAnalysisDate || undefined
questions: useScenarioReplay ? undefined : questionsForRun,
scenarioQuestions: useScenarioReplay ? questionsForRun : undefined,
scenarioTitle: useScenarioReplay ? generation.title ?? undefined : undefined,
analysisDate: useScenarioReplay ? undefined : effectiveAnalysisDate || undefined
});
const liveJob = payload.job;
@@ -1307,7 +1488,11 @@ 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}` : "") +
(effectiveAnalysisDate ? `, analysis_date=${effectiveAnalysisDate}` : ", analysis_date=current_state")
(useScenarioReplay
? ", replay_mode=saved_user_session_scenario"
: effectiveAnalysisDate
? `, analysis_date=${effectiveAnalysisDate}`
: ", analysis_date=current_state")
);
void pollAsyncJobStatus(liveJob.job_id);
} catch (error) {
@@ -1506,6 +1691,97 @@ export function AutoRunsHistoryPanel({
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 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) =>
@@ -1598,11 +1874,11 @@ export function AutoRunsHistoryPanel({
useEffect(() => {
setSelectedAutogenGenerationId((prev) => {
if (autoGenHistory.length === 0) return "";
if (prev && autoGenHistory.some((item) => item.generation_id === prev)) return prev;
return autoGenHistory[0].generation_id;
if (visibleAutoGenHistory.length === 0) return "";
if (prev && visibleAutoGenHistory.some((item) => item.generation_id === prev)) return prev;
return visibleAutoGenHistory[0].generation_id;
});
}, [autoGenHistory]);
}, [visibleAutoGenHistory]);
useEffect(() => {
if (!selectedAutogenGeneration) {
@@ -1610,7 +1886,7 @@ export function AutoRunsHistoryPanel({
return;
}
setEditableGeneratedQuestions([...selectedAutogenGeneration.questions]);
}, [selectedAutogenGeneration?.generation_id]);
}, [selectedAutogenGeneration]);
useEffect(() => {
setLimitInput(String(filters.limit));
@@ -1708,7 +1984,9 @@ export function AutoRunsHistoryPanel({
return {
...prev,
mode:
parsed.autoGenSettings?.mode === "codex_creative" || parsed.autoGenSettings?.mode === "qwen_seed"
parsed.autoGenSettings?.mode === "codex_creative" ||
parsed.autoGenSettings?.mode === "qwen_seed" ||
parsed.autoGenSettings?.mode === "saved_user_sessions"
? parsed.autoGenSettings.mode
: prev.mode,
count:
@@ -1949,140 +2227,155 @@ export function AutoRunsHistoryPanel({
</div>
</div>
<h4>Автогенерация вопросов</h4>
<h4>Автопрогоны</h4>
<div className="autoruns-form-grid">
<label>
Режим генерации
Режимы
<select
value={autoGenSettings.mode}
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, mode: event.target.value as AutoGenMode }))}
>
<option value="codex_creative">codex_creative</option>
<option value="qwen_seed">qwen_seed</option>
<option value="saved_user_sessions">Пользовательские сессии</option>
</select>
</label>
<label>
Кол-во
<input
type="number"
min={1}
max={200}
value={autogenCountInput}
onChange={(event) => {
const raw = event.target.value;
if (raw === "" || /^\d+$/.test(raw)) {
setAutogenCountInput(raw);
}
}}
onBlur={(event) => commitAutogenCountInput(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
commitAutogenCountInput((event.target as HTMLInputElement).value);
}
}}
/>
</label>
<label>
Личность автогенерации
<select
value={autoGenSettings.personalityId}
onChange={(event) =>
setAutoGenSettings((prev) => ({
...prev,
personalityId: event.target.value as AutoGenPersonalityId
}))
}
>
{autogenPersonalities.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
</select>
</label>
<label>
Кто генерирует
<input
value={autoGenSettings.generatedBy}
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, generatedBy: event.target.value }))}
placeholder="manual_reviewer"
/>
</label>
<label className="full-width">
Промпт личности
<textarea
className="autoruns-personality-prompt"
value={autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? ""}
onChange={(event) =>
setAutoGenSettings((prev) => ({
...prev,
personalityPrompts: {
...prev.personalityPrompts,
[prev.personalityId]: event.target.value
{!isSavedUserSessionsMode ? (
<>
<label>
Кол-во
<input
type="number"
min={1}
max={200}
value={autogenCountInput}
onChange={(event) => {
const raw = event.target.value;
if (raw === "" || /^\d+$/.test(raw)) {
setAutogenCountInput(raw);
}
}}
onBlur={(event) => commitAutogenCountInput(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
commitAutogenCountInput((event.target as HTMLInputElement).value);
}
}}
/>
</label>
<label>
Личность автогенерации
<select
value={autoGenSettings.personalityId}
onChange={(event) =>
setAutoGenSettings((prev) => ({
...prev,
personalityId: event.target.value as AutoGenPersonalityId
}))
}
}))
}
placeholder="Текст промпта для выбранной личности автогенерации"
style={{ height: `${autogenPersonalityPromptHeight}px` }}
onMouseUp={captureAutogenPromptHeight}
onTouchEnd={captureAutogenPromptHeight}
/>
</label>
<label className="checkbox-row">
<input
type="checkbox"
checked={autoGenSettings.persistToEvalCases}
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, persistToEvalCases: event.target.checked }))}
/>
Сохранять кейс-сет в `eval_cases`
</label>
>
{autogenPersonalities.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
</select>
</label>
<label>
Кто генерирует
<input
value={autoGenSettings.generatedBy}
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, generatedBy: event.target.value }))}
placeholder="manual_reviewer"
/>
</label>
<label className="full-width">
Промпт личности
<textarea
className="autoruns-personality-prompt"
value={autoGenSettings.personalityPrompts[autoGenSettings.personalityId] ?? ""}
onChange={(event) =>
setAutoGenSettings((prev) => ({
...prev,
personalityPrompts: {
...prev.personalityPrompts,
[prev.personalityId]: event.target.value
}
}))
}
placeholder="Текст промпта для выбранной личности автогенерации"
style={{ height: `${autogenPersonalityPromptHeight}px` }}
onMouseUp={captureAutogenPromptHeight}
onTouchEnd={captureAutogenPromptHeight}
/>
</label>
<label className="checkbox-row">
<input
type="checkbox"
checked={autoGenSettings.persistToEvalCases}
onChange={(event) => setAutoGenSettings((prev) => ({ ...prev, persistToEvalCases: event.target.checked }))}
/>
Сохранять кейс-сет в `eval_cases`
</label>
</>
) : null}
</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>
{!isSavedUserSessionsMode ? (
<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>
</div>
) : null}
<div className="button-row">
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
</button>
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
</button>
{!isSavedUserSessionsMode ? (
<>
<button type="button" disabled={autoGenBusy} onClick={() => void generateAutogenBatch()}>
{autoGenBusy ? "Генерирую..." : "Сгенерировать пачку"}
</button>
<button type="button" className="tab" disabled={autogenHistoryBusy} onClick={() => void loadAutoGenHistory()}>
{autogenHistoryBusy ? "Обновляю..." : "Обновить историю"}
</button>
</>
) : null}
<button
type="button"
className="autoruns-run-launch-btn"
disabled={autogenRunBusy || editableGeneratedQuestions.length === 0}
disabled={autogenRunBusy || editableGeneratedQuestions.length === 0 || !selectedAutogenGeneration}
onClick={() => void runAutogenCampaign()}
>
{autogenRunBusy ? "Запускаю..." : "Запустить прогоны"}
{autogenRunBusy ? "Запускаю..." : "Запустить прогон"}
</button>
</div>
<div className="autoruns-form-grid">
<label className="full-width">
Кейс-сет для запуска
{isSavedUserSessionsMode ? "Сохраненная сессия" : "Кейс-сет для запуска"}
<select
value={selectedAutogenGenerationId}
onChange={(event) => setSelectedAutogenGenerationId(event.target.value)}
disabled={autoGenHistory.length === 0}
disabled={visibleAutoGenHistory.length === 0}
>
{autoGenHistory.length === 0 ? <option value="">нет генераций</option> : null}
{autoGenHistory.map((item) => (
{visibleAutoGenHistory.length === 0 ? (
<option value="">
{isSavedUserSessionsMode ? "нет сохраненных сессий" : "нет генераций"}
</option>
) : null}
{visibleAutoGenHistory.map((item) => (
<option key={item.generation_id} value={item.generation_id}>
{formatDateTime(item.created_at)} | {item.mode} | {item.count} | {item.saved_case_set_file ?? "без файла"}
{formatDateTime(item.created_at)} | {item.title ?? formatAutoGenModeLabel(item.mode)} | {item.count}
</option>
))}
</select>
@@ -2101,7 +2394,11 @@ export function AutoRunsHistoryPanel({
</button>
</div>
{editableGeneratedQuestions.length === 0 ? (
<p className="muted">Список вопросов пуст. Сгенерируйте пачку или восстановите из выбранной генерации.</p>
<p className="muted">
{isSavedUserSessionsMode
? "Список вопросов пуст. Сначала сохраните живую пользовательскую сессию."
: "Список вопросов пуст. Сгенерируйте пачку или восстановите из выбранной генерации."}
</p>
) : (
<div className="autoruns-generated-questions-list">
{editableGeneratedQuestions.map((question, index) => (
@@ -2110,41 +2407,93 @@ export function AutoRunsHistoryPanel({
<button
type="button"
className="autoruns-remove-question-btn"
onClick={() =>
setEditableGeneratedQuestions((prev) => prev.filter((_, itemIndex) => itemIndex !== index))
}
onClick={() => {
if (isSavedUserSessionsMode) {
requestDeleteSavedSessionQuestion(index);
return;
}
setEditableGeneratedQuestions((prev) => prev.filter((_, itemIndex) => itemIndex !== index));
}}
title="Удалить вопрос из запуска"
aria-label="Удалить вопрос из запуска"
>
+
×
</button>
</div>
))}
</div>
)}
</div>
<p className="muted">Запуск выполняет `assistant_stage1` eval по выбранному кейс-сету.</p>
<p className="muted">
{isSavedUserSessionsMode
? "Запуск воспроизводит сохраненную пользовательскую сессию как один последовательный multi-turn сценарий assistant_stage1."
: "Запуск выполняет `assistant_stage1` eval по выбранному кейс-сету."}
</p>
<div className="autoruns-autogen-list">
{autogenHistoryBusy ? <p className="muted">Загружаю историю автогенераций...</p> : null}
{!autogenHistoryBusy && autoGenHistory.length === 0 ? <p className="muted">История автогенераций пока пустая.</p> : null}
{autoGenHistory.slice(0, 30).map((item) => (
{autogenHistoryBusy ? (
<p className="muted">
{isSavedUserSessionsMode ? "Загружаю сохраненные пользовательские сессии..." : "Загружаю историю автогенераций..."}
</p>
) : null}
{!autogenHistoryBusy && visibleAutoGenHistory.length === 0 ? (
<p className="muted">
{isSavedUserSessionsMode ? "Сохраненные пользовательские сессии пока пусты." : "История автогенераций пока пустая."}
</p>
) : null}
{visibleAutoGenHistory.slice(0, 30).map((item) => (
<article
key={item.generation_id}
className={selectedAutogenGenerationId === item.generation_id ? "autoruns-autogen-item selected" : "autoruns-autogen-item"}
onClick={() => setSelectedAutogenGenerationId(item.generation_id)}
>
<header>
<strong>{formatDateTime(item.created_at)}</strong>
<span>{item.mode}</span>
<strong>{item.title ?? formatDateTime(item.created_at)}</strong>
<div className="autoruns-autogen-card-actions">
<span>{formatDateTime(item.created_at)}</span>
<button
type="button"
className="autoruns-autogen-delete-btn"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
openAutoGenDeleteModal(item);
}}
title="Удалить сохраненный набор"
aria-label={`Удалить набор ${item.generation_id}`}
>
×
</button>
</div>
</header>
<div className="autoruns-run-meta">
id={item.generation_id} | count={item.count}
<div className="autoruns-run-meta autoruns-run-id-row">
<span>{item.generation_id}</span>
<span
role="button"
tabIndex={0}
className="autoruns-copy-run-id-btn"
onClick={(event) => void copyIdentifierToClipboard(event, item.generation_id, "set id")}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
void copyIdentifierToClipboard(event, item.generation_id, "set id");
}
}}
title="Скопировать id набора"
aria-label={`Скопировать id набора ${item.generation_id}`}
>
<CopyOutlineIcon />
</span>
</div>
<div className="autoruns-run-meta">
домен={item.domain ?? "общий"}
{item.generated_by ? ` | автор=${item.generated_by}` : ""}
режим={formatAutoGenModeLabel(item.mode)} | count={item.count}
</div>
{item.domain || item.generated_by ? (
<div className="autoruns-run-meta">
{item.domain ? `домен=${item.domain}` : "домен=общий"}
{item.generated_by ? ` | автор=${item.generated_by}` : ""}
</div>
) : null}
{item.saved_case_set_file ? <div className="autoruns-run-meta">кейс-сет={item.saved_case_set_file}</div> : null}
{(item.questions ?? []).length > 0 ? <p>{item.questions[0]}</p> : null}
</article>
@@ -2219,11 +2568,11 @@ export function AutoRunsHistoryPanel({
role="button"
tabIndex={0}
className="autoruns-copy-run-id-btn"
onClick={(event) => void copyRunIdToClipboard(event, run.run_id)}
onClick={(event) => void copyIdentifierToClipboard(event, run.run_id, "run id")}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
void copyRunIdToClipboard(event, run.run_id);
void copyIdentifierToClipboard(event, run.run_id, "run id");
}
}}
title="Скопировать run id"
@@ -2424,9 +2773,13 @@ export function AutoRunsHistoryPanel({
onUseMockChange={setAssistantLiveUseMock}
onSend={sendAssistantLiveMessage}
onClear={resetAssistantLiveSession}
onSaveSession={openAssistantLiveSaveModal}
busy={assistantLiveBusy}
saveBusy={assistantLiveSaveModal.saving}
saveDisabled={!assistantLiveSessionId.trim() || assistantLiveConversation.length === 0 || assistantLiveBusy}
statusText={assistantLiveStatus}
errorMessage={assistantLiveError}
showSaveAction
showCommentAction
onCommentAssistantMessage={openAssistantLiveCommentModal}
isAssistantMessageCommented={isAssistantLiveMessageCommented}
@@ -2435,42 +2788,6 @@ export function AutoRunsHistoryPanel({
</div>
) : null}
{showDecompositionMode ? (
<section className="autoruns-col">
<div className="autoruns-col-header">
<h3>Режим декомпозиции</h3>
</div>
<div className="autoruns-meta-list">
<div>
<span>кейс:</span>
<strong>{activeCase?.case_id ?? "нет данных"}</strong>
</div>
<div>
<span>домен:</span>
<strong>{activeCase?.domain ?? "нет данных"}</strong>
</div>
<div>
<span>класс запроса:</span>
<strong>{activeCase?.query_class ?? "нет данных"}</strong>
</div>
<div>
<span>trace:</span>
<strong>{activeCase?.trace_id ?? "нет данных"}</strong>
</div>
</div>
<h4>Шаги декомпозиции</h4>
{(dialog?.decomposition.length ?? 0) > 0 ? (
<ol className="autoruns-decomposition-list">
{(dialog?.decomposition ?? []).map((item, index) => (
<li key={`${index}-${item.slice(0, 24)}`}>{item}</li>
))}
</ol>
) : (
<p className="muted">В логах кейса нет явной декомпозиции.</p>
)}
</section>
) : null}
{showProgressMode ? (
<section className="autoruns-col">
<div className="autoruns-col-header">
@@ -2746,6 +3063,104 @@ export function AutoRunsHistoryPanel({
) : null}
</div>
{assistantLiveSaveModal.open ? (
<div
className="autoruns-comment-modal-backdrop"
onClick={(event) => {
if (event.target === event.currentTarget) {
closeAssistantLiveSaveModal();
}
}}
>
<div className="autoruns-comment-modal">
<h3>Сохранить ручную сессию</h3>
<p className="muted">Технический чат будет сохранен в автопрогоны как пользовательская multi-turn сессия.</p>
<label>
Название
<input
value={assistantLiveSaveModal.title}
onChange={(event) => setAssistantLiveSaveModal((prev) => ({ ...prev, title: event.target.value }))}
placeholder="Например: НДС и склад на март 2020"
disabled={assistantLiveSaveModal.saving}
/>
</label>
{assistantLiveSaveModal.error ? <p className="error-text">{assistantLiveSaveModal.error}</p> : null}
<div className="button-row">
<button type="button" onClick={() => void submitAssistantLiveSaveModal()} disabled={assistantLiveSaveModal.saving}>
{assistantLiveSaveModal.saving ? "Сохраняю..." : "Сохранить"}
</button>
<button type="button" className="tab" onClick={() => closeAssistantLiveSaveModal()} disabled={assistantLiveSaveModal.saving}>
Отмена
</button>
</div>
</div>
</div>
) : null}
{savedSessionQuestionDeleteModal.open ? (
<div
className="autoruns-comment-modal-backdrop"
onClick={(event) => {
if (event.target === event.currentTarget) {
closeSavedSessionQuestionDeleteModal();
}
}}
>
<div className="autoruns-comment-modal">
<h3>Удалить вопрос</h3>
<p className="muted">Действительно удалить вопрос из сохраненной пользовательской сессии?</p>
<p className="autoruns-comment-quote">{savedSessionQuestionDeleteModal.questionText}</p>
{savedSessionQuestionDeleteModal.error ? <p className="error-text">{savedSessionQuestionDeleteModal.error}</p> : null}
<div className="button-row">
<button type="button" onClick={() => void submitSavedSessionQuestionDelete()} disabled={savedSessionQuestionDeleteModal.saving}>
{savedSessionQuestionDeleteModal.saving ? "Удаляю..." : "Да"}
</button>
<button
type="button"
className="tab"
onClick={() => closeSavedSessionQuestionDeleteModal()}
disabled={savedSessionQuestionDeleteModal.saving}
>
Нет
</button>
</div>
</div>
</div>
) : null}
{autoGenDeleteModal.open ? (
<div
className="autoruns-comment-modal-backdrop"
onClick={(event) => {
if (event.target === event.currentTarget) {
closeAutoGenDeleteModal();
}
}}
>
<div className="autoruns-comment-modal">
<h3>Удалить сохраненный набор</h3>
<p className="muted">Будет удалена карточка истории и связанный файл кейс-сета на бэке.</p>
<p className="autoruns-comment-quote">{autoGenDeleteModal.title}</p>
{autoGenDeleteModal.error ? <p className="error-text">{autoGenDeleteModal.error}</p> : null}
<div className="button-row">
<button type="button" onClick={() => void submitAutoGenDeleteModal()} disabled={autoGenDeleteModal.saving}>
{autoGenDeleteModal.saving ? "Удаляю..." : "Да"}
</button>
<button type="button" className="tab" onClick={() => closeAutoGenDeleteModal()} disabled={autoGenDeleteModal.saving}>
Нет
</button>
</div>
</div>
</div>
) : null}
{assistantLiveCommentModal.open ? (
<div
className="autoruns-comment-modal-backdrop"