ЮИ - редактура вопросов в АВТОПРОГОНАХ

This commit is contained in:
2026-04-18 10:13:16 +03:00
parent 0431595542
commit 9872ef5446
35 changed files with 1088 additions and 44 deletions
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState, type SyntheticEvent } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEvent, type SyntheticEvent } from "react";
import { apiClient } from "../api/client";
import type {
AssistantConversationItem,
@@ -553,6 +553,22 @@ function CopyOutlineIcon() {
);
}
function QuestionGripIcon() {
return (
<svg className="autoruns-question-grip-svg" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<circle cx="4" cy="4" r="1" />
<circle cx="8" cy="4" r="1" />
<circle cx="12" cy="4" r="1" />
<circle cx="4" cy="8" r="1" />
<circle cx="8" cy="8" r="1" />
<circle cx="12" cy="8" r="1" />
<circle cx="4" cy="12" r="1" />
<circle cx="8" cy="12" r="1" />
<circle cx="12" cy="12" r="1" />
</svg>
);
}
export function AutoRunsHistoryPanel({
connection,
modelOptions,
@@ -605,6 +621,11 @@ export function AutoRunsHistoryPanel({
const [autoGenHistory, setAutoGenHistory] = useState<AutoGenHistoryRecord[]>([]);
const [selectedAutogenGenerationId, setSelectedAutogenGenerationId] = useState("");
const [editableGeneratedQuestions, setEditableGeneratedQuestions] = useState<string[]>([]);
const [generatedQuestionsBusy, setGeneratedQuestionsBusy] = useState(false);
const [editingQuestionIndex, setEditingQuestionIndex] = useState<number | null>(null);
const [editingQuestionDraft, setEditingQuestionDraft] = useState("");
const [draggingQuestionIndex, setDraggingQuestionIndex] = useState<number | null>(null);
const [dragOverQuestionIndex, setDragOverQuestionIndex] = useState<number | null>(null);
const [activeAsyncJob, setActiveAsyncJob] = useState<AsyncEvalRunJob | null>(null);
const [postAnalysis, setPostAnalysis] = useState<AutoRunPostAnalysisResponse | null>(null);
const [autoGenBusy, setAutoGenBusy] = useState(false);
@@ -674,6 +695,7 @@ export function AutoRunsHistoryPanel({
const initialLoadDoneRef = useRef(false);
const asyncJobPollTimerRef = useRef<number | null>(null);
const questionEditorRef = useRef<HTMLInputElement | null>(null);
const isSavedUserSessionsMode = autoGenSettings.mode === "saved_user_sessions";
const selectedPersonality = useMemo(
() => autogenPersonalities.find((item) => item.id === autoGenSettings.personalityId) ?? autogenPersonalities[0] ?? AUTOGEN_PERSONALITIES[0],
@@ -1772,6 +1794,192 @@ export function AutoRunsHistoryPanel({
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<HTMLInputElement>) => {
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<HTMLButtonElement>, 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<HTMLDivElement>, questionIndex: number) => {
event.preventDefault();
if (dragOverQuestionIndex !== questionIndex) {
setDragOverQuestionIndex(questionIndex);
}
event.dataTransfer.dropEffect = "move";
},
[dragOverQuestionIndex]
);
const handleQuestionDrop = useCallback(
async (event: DragEvent<HTMLDivElement>, 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 openAutoGenDeleteModal = useCallback((item: AutoGenHistoryRecord) => {
setAutoGenDeleteModal({
open: true,
@@ -1905,10 +2113,27 @@ export function AutoRunsHistoryPanel({
useEffect(() => {
if (!selectedAutogenGeneration) {
setEditableGeneratedQuestions([]);
stopQuestionEdit();
setDraggingQuestionIndex(null);
setDragOverQuestionIndex(null);
return;
}
setEditableGeneratedQuestions([...selectedAutogenGeneration.questions]);
}, [selectedAutogenGeneration]);
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(() => {
setLimitInput(String(filters.limit));
@@ -2403,6 +2628,9 @@ export function AutoRunsHistoryPanel({
</select>
</label>
</div>
{false ? (
<>
{/* generated questions editor */}
<div className="autoruns-generated-questions">
<div className="autoruns-generated-questions-head">
<strong>Вопросы к запуску: {editableGeneratedQuestions.length}</strong>
@@ -2451,6 +2679,99 @@ export function AutoRunsHistoryPanel({
? "Запуск воспроизводит сохраненную пользовательскую сессию как один последовательный multi-turn сценарий assistant_stage1."
: "Запуск выполняет `assistant_stage1` eval по выбранному кейс-сету."}
</p>
</>
) : (
<>
<div className="autoruns-generated-questions">
<div className="autoruns-generated-questions-head">
<strong>Вопросы к запуску: {editableGeneratedQuestions.length}</strong>
</div>
{editableGeneratedQuestions.length === 0 ? (
<p className="muted">
{isSavedUserSessionsMode
? "Список вопросов пуст. Сначала сохраните живую пользовательскую сессию."
: "Список вопросов пуст. Сгенерируйте пачку или добавьте вопрос вручную."}
</p>
) : (
<div className="autoruns-generated-questions-list">
{editableGeneratedQuestions.map((question, index) => (
<div
key={`${index}-${question.slice(0, 24)}`}
className={[
"autoruns-generated-question-item",
dragOverQuestionIndex === index ? "drag-over" : "",
draggingQuestionIndex === index ? "dragging" : "",
editingQuestionIndex === index ? "editing" : ""
].filter(Boolean).join(" ")}
onDragOver={(event) => handleQuestionDragOver(event, index)}
onDrop={(event) => void handleQuestionDrop(event, index)}
>
<button
type="button"
className="autoruns-question-grip-btn"
draggable={!generatedQuestionsBusy && editingQuestionIndex !== index}
disabled={generatedQuestionsBusy || editingQuestionIndex === index}
onDragStart={(event) => handleQuestionDragStart(event, index)}
onDragEnd={handleQuestionDragEnd}
title="Перетащить вопрос"
aria-label={`Перетащить вопрос ${index + 1}`}
>
<QuestionGripIcon />
</button>
{editingQuestionIndex === index ? (
<>
<input
ref={questionEditorRef}
className="autoruns-generated-question-input"
value={editingQuestionDraft}
onChange={(event) => setEditingQuestionDraft(event.target.value)}
onBlur={handleQuestionEditorBlur}
onKeyDown={handleQuestionEditorKeyDown}
placeholder="Текст вопроса"
disabled={generatedQuestionsBusy}
/>
<button
type="button"
className="autoruns-remove-question-btn"
onMouseDown={(event) => event.preventDefault()}
onClick={() => void handleDeleteGeneratedQuestion(index)}
title="Удалить вопрос"
aria-label={`Удалить вопрос ${index + 1}`}
disabled={generatedQuestionsBusy}
>
×
</button>
</>
) : (
<button
type="button"
className="autoruns-generated-question-text"
onDoubleClick={() => startQuestionEdit(index)}
title="Двойной клик для редактирования"
>
{index + 1}. {question}
</button>
)}
</div>
))}
</div>
)}
<button
type="button"
className="autoruns-add-question-btn"
onClick={() => void handleAddGeneratedQuestion()}
disabled={!selectedAutogenGeneration || generatedQuestionsBusy}
>
+
</button>
</div>
{isSavedUserSessionsMode ? (
<h4>Сохраненные пользовательские сессии</h4>
) : (
<p className="muted">Запуск выполняет `assistant_stage1` eval по выбранному кейс-сету.</p>
)}
</>
)}
<div className="autoruns-autogen-list">
{autogenHistoryBusy ? (
+129 -7
View File
@@ -1535,24 +1535,137 @@ button:disabled {
.autoruns-generated-question-item {
position: relative;
display: block;
display: grid;
grid-template-columns: 22px minmax(0, 1fr) auto;
align-items: start;
gap: 8px;
border: none;
border-radius: 9px;
background: rgb(var(--rgb-surface-focus));
padding: 7px 30px 7px 8px;
padding: 7px 8px;
font-size: 0.78rem;
transition: background 0.15s ease, outline-color 0.15s ease, opacity 0.15s ease;
}
.autoruns-generated-question-item span {
display: block;
.autoruns-generated-question-item.drag-over {
outline: 1px solid rgba(var(--rgb-active), 0.75);
}
.autoruns-generated-question-item.dragging {
opacity: 0.72;
}
.autoruns-generated-question-item.editing {
background: rgb(var(--rgb-active));
color: rgb(var(--rgb-active-text));
}
.autoruns-question-grip-btn {
width: 18px;
min-width: 18px;
height: 18px;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: grab;
margin-top: 1px;
}
.autoruns-question-grip-btn:hover:not(:disabled) {
color: rgb(var(--rgb-text-main));
background: rgba(var(--rgb-background), 0.3);
}
.autoruns-generated-question-item.editing .autoruns-question-grip-btn {
color: rgba(var(--rgb-active-text), 0.9);
}
.autoruns-generated-question-item.editing .autoruns-question-grip-btn:hover:not(:disabled) {
color: rgb(var(--rgb-active-text));
background: rgba(var(--rgb-active-text), 0.14);
}
.autoruns-question-grip-btn:disabled {
cursor: default;
opacity: 0.45;
}
.autoruns-question-grip-svg {
width: 14px;
height: 14px;
fill: currentColor;
}
.autoruns-generated-question-text {
border: none;
background: transparent;
color: rgb(var(--rgb-text-main));
padding: 0;
margin: 0;
text-align: left;
font: inherit;
white-space: pre-wrap;
line-height: 1.4;
cursor: text;
}
.autoruns-generated-question-text:hover {
color: rgb(var(--rgb-active));
}
.autoruns-generated-question-input {
width: 100%;
min-width: 0;
border: none;
border-radius: 8px;
background: rgba(var(--rgb-background), 0.55);
color: rgb(var(--rgb-text-main));
padding: 6px 8px;
font: inherit;
line-height: 1.4;
}
.autoruns-generated-question-input:focus {
outline: none;
}
.autoruns-generated-question-item.editing .autoruns-generated-question-input {
background: rgba(var(--rgb-active-text), 0.14);
color: rgb(var(--rgb-active-text));
}
.autoruns-generated-question-item.editing .autoruns-generated-question-input::placeholder {
color: rgba(var(--rgb-active-text), 0.78);
}
.autoruns-add-question-btn {
width: 100%;
min-height: 30px;
border-radius: 8px;
border: none;
background: rgb(var(--rgb-surface-focus));
color: rgb(var(--rgb-text-main));
font-size: 1.1rem;
font-weight: 700;
line-height: 1;
}
.autoruns-add-question-btn:hover:not(:disabled) {
background: rgb(var(--rgb-active));
color: rgb(var(--rgb-active-text));
}
.autoruns-add-question-btn:disabled {
opacity: 0.5;
cursor: default;
}
.autoruns-remove-question-btn {
position: absolute;
top: 6px;
right: 6px;
flex: 0 0 auto;
border: none;
border-radius: 0;
@@ -1570,6 +1683,7 @@ button:disabled {
justify-content: center;
box-shadow: none;
transition: color 0.15s ease;
align-self: start;
}
.autoruns-remove-question-btn:hover {
@@ -1578,6 +1692,14 @@ button:disabled {
box-shadow: none;
}
.autoruns-generated-question-item.editing .autoruns-remove-question-btn {
color: rgb(var(--rgb-active-text));
}
.autoruns-generated-question-item.editing .autoruns-remove-question-btn:hover {
color: rgba(var(--rgb-active-text), 0.82);
}
.autoruns-remove-question-btn:focus-visible {
outline: none;
}