ЮИ - Добавить удаление сохраненных наборов автопрогонов с удалением файлов на бэке
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user