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

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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NDC AI Normalizer Playground</title>
<script type="module" crossorigin src="/assets/index-C8U6PD78.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CfrZGsZo.css">
<script type="module" crossorigin src="/assets/index-3F56oUw0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DNDajOYc.css">
</head>
<body>
<div id="root"></div>
@@ -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;
}