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

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
+290 -12
View File
@@ -93,6 +93,16 @@ function clampInt(value, min, max, fallback) {
return max;
return rounded;
}
function isAutoGenMode(value) {
return value === "qwen_seed" || value === "codex_creative" || value === "saved_user_sessions";
}
function parseAutoGenTitle(value) {
const title = toStringSafe(value);
if (!title) {
return null;
}
return title.slice(0, 160);
}
function parseManualCaseDecision(value, fallback = "needs_dialog_policy_fix") {
const normalized = toStringSafe(value);
if (!normalized)
@@ -151,15 +161,11 @@ function readAutoGenHistory() {
.map((item) => ({
generation_id: toStringSafe(item.generation_id) ?? "",
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
mode: toStringSafe(item.mode) ?? "codex_creative",
mode: isAutoGenMode(toStringSafe(item.mode)) ? toStringSafe(item.mode) : "codex_creative",
title: parseAutoGenTitle(item.title),
count: clampInt(toNumberSafe(item.count), 1, 300, 20),
domain: toStringSafe(item.domain),
questions: toArray(item.questions)
.map((q) => toStringSafe(q))
.filter((q) => q !== null)
.map((q) => sanitizeGeneratedQuestion(q))
.filter((q) => q.length > 0)
.slice(0, 500),
questions: parseAssistantSessionQuestions(item.questions),
generated_by: toStringSafe(item.generated_by),
saved_case_set_file: toStringSafe(item.saved_case_set_file),
context: toRecord(item.context)
@@ -174,7 +180,10 @@ function readAutoGenHistory() {
autogen_personality_id: toStringSafe(toRecord(item.context)?.autogen_personality_id),
autogen_personality_prompt: toStringSafe(toRecord(item.context)?.autogen_personality_prompt)
? repairAutogenMojibake(String(toRecord(item.context)?.autogen_personality_prompt))
: null
: null,
source_session_id: toStringSafe(toRecord(item.context)?.source_session_id),
saved_session_file: toStringSafe(toRecord(item.context)?.saved_session_file),
saved_case_set_kind: toStringSafe(toRecord(item.context)?.saved_case_set_kind)
}
: null
}))
@@ -1057,7 +1066,7 @@ function parseDecisionFilter(value) {
}
function parseAutoGenMode(value) {
const normalized = toStringSafe(value)?.toLowerCase() ?? "";
if (normalized === "qwen_seed" || normalized === "codex_creative") {
if (normalized === "qwen_seed" || normalized === "codex_creative" || normalized === "saved_user_sessions") {
return normalized;
}
return "codex_creative";
@@ -1150,6 +1159,12 @@ function sanitizeGeneratedQuestion(value) {
.replace(/\s+/g, " ")
.trim();
}
function parseAssistantSessionQuestions(value) {
return toArray(value)
.map((item) => sanitizeGeneratedQuestion(typeof item === "string" ? item : ""))
.filter((item) => item.length > 0)
.slice(0, 500);
}
const AUTOGEN_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
const AUTOGEN_QUESTION_TAIL_PATTERNS = [
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
@@ -1413,6 +1428,18 @@ function buildAutogenCaseSetFileName(mode, generationId) {
].join("");
return `assistant_autogen_${mode}_${stamp}_${generationId}.json`;
}
function buildSavedAssistantSessionSnapshotFileName(generationId) {
const now = new Date();
const stamp = [
now.getUTCFullYear(),
String(now.getUTCMonth() + 1).padStart(2, "0"),
String(now.getUTCDate()).padStart(2, "0"),
String(now.getUTCHours()).padStart(2, "0"),
String(now.getUTCMinutes()).padStart(2, "0"),
String(now.getUTCSeconds()).padStart(2, "0")
].join("");
return `assistant_saved_session_${stamp}_${generationId}.json`;
}
function buildAutogenCaseSetPayload(input) {
const normalizedQuestions = Array.from(new Set(input.questions.map((item) => sanitizeGeneratedQuestion(item)).filter((item) => item.length > 0)));
const cases = normalizedQuestions.map((question, index) => ({
@@ -1439,6 +1466,99 @@ function buildAutogenCaseSetPayload(input) {
cases
};
}
function buildSavedSessionCaseSetPayload(input) {
const questions = parseAssistantSessionQuestions(input.questions);
const turns = questions.map((question) => ({
user_message: question
}));
const caseId = "SAVED-001";
return {
suite_id: `assistant_saved_session_${input.generationId}`,
suite_version: "0.1.0",
schema_version: "assistant_saved_session_suite_v0_1",
generated_at: new Date().toISOString(),
generation_id: input.generationId,
mode: "saved_user_sessions",
title: input.title,
scenario_count: turns.length > 0 ? 1 : 0,
case_ids: turns.length > 0 ? [caseId] : [],
cases: turns.length > 0
? [
{
case_id: caseId,
scenario_tag: "saved_user_sessions",
title: input.title,
question_type: turns.length > 1 ? "followup" : "direct",
broadness_level: "medium",
turns
}
]
: []
};
}
function ensureDirSync(targetDir) {
if (!fs_1.default.existsSync(targetDir)) {
fs_1.default.mkdirSync(targetDir, { recursive: true });
}
}
function writeJsonFile(targetPath, payload) {
ensureDirSync(path_1.default.dirname(targetPath));
fs_1.default.writeFileSync(targetPath, JSON.stringify(payload, null, 2), "utf-8");
}
function rewriteAutoGenCaseSetFile(record) {
const caseSetFile = toStringSafe(record.saved_case_set_file);
if (!caseSetFile) {
return null;
}
const targetPath = path_1.default.resolve(config_1.EVAL_CASES_DIR, caseSetFile);
const payload = record.mode === "saved_user_sessions"
? buildSavedSessionCaseSetPayload({
generationId: record.generation_id,
title: record.title,
questions: record.questions
})
: buildAutogenCaseSetPayload({
generationId: record.generation_id,
mode: record.mode,
domain: record.domain,
questions: record.questions
});
writeJsonFile(targetPath, payload);
return caseSetFile;
}
function writeSavedAssistantSessionSnapshot(input) {
const fileName = buildSavedAssistantSessionSnapshotFileName(input.generationId);
const targetPath = path_1.default.resolve(path_1.default.dirname(config_1.AUTORUN_GENERATOR_HISTORY_FILE), "saved_sessions", fileName);
writeJsonFile(targetPath, {
saved_at: new Date().toISOString(),
generation_id: input.generationId,
mode: "saved_user_sessions",
title: input.title,
source_session_id: input.sessionId,
questions: input.questions,
session: input.session
});
return fileName;
}
function resolveFileInsideDir(baseDir, fileName) {
const normalized = toStringSafe(fileName);
if (!normalized) {
return null;
}
const targetPath = path_1.default.resolve(baseDir, normalized);
const relative = path_1.default.relative(baseDir, targetPath);
if (relative.startsWith("..") || path_1.default.isAbsolute(relative)) {
return null;
}
return targetPath;
}
function safeDeleteFile(targetPath) {
if (!targetPath || !fs_1.default.existsSync(targetPath)) {
return null;
}
fs_1.default.unlinkSync(targetPath);
return targetPath;
}
function collectPostAnalysis(annotations, runMap, limitPerQueue) {
const byDecision = {};
const byQueue = {};
@@ -1522,7 +1642,7 @@ function collectPostAnalysis(annotations, runMap, limitPerQueue) {
].slice(0, 60)
};
}
function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIResponsesClient()) {
function buildAutoRunsRouter(services, openaiClient = new openaiResponsesClient_1.OpenAIResponsesClient()) {
const router = (0, express_1.Router)();
router.get("/api/autoruns/history", (req, res) => {
const filters = parseFilters(req.query);
@@ -1884,7 +2004,7 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
try {
const limit = clampInt(toNumberSafe(req.query.limit), 1, 500, 120);
const rawMode = toStringSafe(req.query.mode);
const includeAllModes = !rawMode || !["qwen_seed", "codex_creative"].includes(rawMode);
const includeAllModes = !rawMode || !isAutoGenMode(rawMode);
const modeFilter = rawMode ?? "codex_creative";
const items = readAutoGenHistory()
.filter((item) => (includeAllModes ? true : item.mode === modeFilter))
@@ -1911,6 +2031,157 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
next(error);
}
});
router.post("/api/autoruns/autogen/save-assistant-session", (req, res, next) => {
try {
const body = toRecord(req.body);
if (!body) {
throw new http_1.ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "JSON body is required", 400);
}
const sessionId = toStringSafe(body.session_id);
const title = parseAutoGenTitle(body.title);
const generatedBy = parseAnnotationAuthor(body.generated_by);
const context = toRecord(body.context);
if (!sessionId) {
throw new http_1.ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "session_id is required", 400);
}
if (!title) {
throw new http_1.ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "title is required", 400);
}
const session = services.assistantService.getSession(sessionId);
if (!session) {
throw new http_1.ApiError("ASSISTANT_SESSION_NOT_FOUND", `Session not found: ${sessionId}`, 404);
}
const questions = session.items
.filter((item) => item.role === "user")
.map((item) => sanitizeGeneratedQuestion(item.text))
.filter((item) => item.length > 0);
if (questions.length === 0) {
throw new http_1.ApiError("ASSISTANT_SESSION_EMPTY", "Assistant session has no user questions to save.", 400);
}
const generationId = generateAutogenId();
const caseSetFile = buildAutogenCaseSetFileName("saved_user_sessions", generationId);
const caseSetPath = path_1.default.resolve(config_1.EVAL_CASES_DIR, caseSetFile);
writeJsonFile(caseSetPath, buildSavedSessionCaseSetPayload({
generationId,
title,
questions
}));
const snapshotFile = writeSavedAssistantSessionSnapshot({
generationId,
sessionId,
title,
session: session,
questions
});
const record = {
generation_id: generationId,
created_at: new Date().toISOString(),
mode: "saved_user_sessions",
title,
count: questions.length,
domain: null,
questions,
generated_by: generatedBy,
saved_case_set_file: caseSetFile,
context: {
llm_provider: toStringSafe(context?.llm_provider),
model: toStringSafe(context?.model),
assistant_prompt_version: toStringSafe(context?.assistant_prompt_version),
decomposition_prompt_version: toStringSafe(context?.decomposition_prompt_version),
prompt_fingerprint: toStringSafe(context?.prompt_fingerprint)
? repairAutogenMojibake(String(context?.prompt_fingerprint))
: null,
autogen_personality_id: null,
autogen_personality_prompt: null,
source_session_id: sessionId,
saved_session_file: snapshotFile,
saved_case_set_kind: "assistant_session_scenario"
}
};
const history = readAutoGenHistory();
history.unshift(record);
writeAutoGenHistory(history.slice(0, 500));
(0, http_1.ok)(res, {
ok: true,
generation: record
});
}
catch (error) {
next(error);
}
});
router.patch("/api/autoruns/autogen/history/:generation_id/questions", (req, res, next) => {
try {
const generationId = toStringSafe(req.params.generation_id);
const body = toRecord(req.body);
if (!generationId) {
throw new http_1.ApiError("INVALID_AUTOGEN_GENERATION_ID", "generation_id is required", 400);
}
if (!body) {
throw new http_1.ApiError("INVALID_AUTOGEN_QUESTIONS_PAYLOAD", "JSON body is required", 400);
}
const questions = parseAssistantSessionQuestions(body.questions);
if (questions.length === 0) {
throw new http_1.ApiError("INVALID_AUTOGEN_QUESTIONS_PAYLOAD", "questions must contain at least one item", 400);
}
const history = readAutoGenHistory();
const targetIndex = history.findIndex((item) => item.generation_id === generationId);
if (targetIndex < 0) {
throw new http_1.ApiError("AUTOGEN_GENERATION_NOT_FOUND", `Generation not found: ${generationId}`, 404);
}
const current = history[targetIndex];
const updated = {
...current,
count: questions.length,
questions
};
rewriteAutoGenCaseSetFile(updated);
history[targetIndex] = updated;
writeAutoGenHistory(history);
(0, http_1.ok)(res, {
ok: true,
generation: updated
});
}
catch (error) {
next(error);
}
});
router.delete("/api/autoruns/autogen/history/:generation_id", (req, res, next) => {
try {
const generationId = toStringSafe(req.params.generation_id);
if (!generationId) {
throw new http_1.ApiError("INVALID_AUTOGEN_GENERATION_ID", "generation_id is required", 400);
}
const history = readAutoGenHistory();
const targetIndex = history.findIndex((item) => item.generation_id === generationId);
if (targetIndex < 0) {
throw new http_1.ApiError("AUTOGEN_GENERATION_NOT_FOUND", `Generation not found: ${generationId}`, 404);
}
const target = history[targetIndex];
const deletedFiles = [];
const caseSetPath = resolveFileInsideDir(config_1.EVAL_CASES_DIR, target.saved_case_set_file);
const savedSessionPath = resolveFileInsideDir(path_1.default.resolve(path_1.default.dirname(config_1.AUTORUN_GENERATOR_HISTORY_FILE), "saved_sessions"), target.context?.saved_session_file ?? null);
const deletedCaseSet = safeDeleteFile(caseSetPath);
if (deletedCaseSet) {
deletedFiles.push(deletedCaseSet);
}
const deletedSavedSession = safeDeleteFile(savedSessionPath);
if (deletedSavedSession) {
deletedFiles.push(deletedSavedSession);
}
history.splice(targetIndex, 1);
writeAutoGenHistory(history);
(0, http_1.ok)(res, {
ok: true,
generation_id: generationId,
deleted_files: deletedFiles
});
}
catch (error) {
next(error);
}
});
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
try {
const body = toRecord(req.body);
@@ -1925,6 +2196,9 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
const context = toRecord(body.context);
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
const personalityPrompt = toStringSafe(context?.autogen_personality_prompt);
if (mode === "saved_user_sessions") {
throw new http_1.ApiError("AUTOGEN_MODE_NOT_SUPPORTED", "Use `/api/autoruns/autogen/save-assistant-session` to save user sessions.", 400);
}
let questions = [];
if (mode === "qwen_seed") {
if (!llmConfig) {
@@ -1963,6 +2237,7 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
generation_id: generationId,
created_at: new Date().toISOString(),
mode,
title: null,
count: questions.length,
domain,
questions,
@@ -1980,7 +2255,10 @@ function buildAutoRunsRouter(openaiClient = new openaiResponsesClient_1.OpenAIRe
autogen_personality_id: toStringSafe(context.autogen_personality_id),
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
? repairAutogenMojibake(String(context.autogen_personality_prompt))
: null
: null,
source_session_id: null,
saved_session_file: null,
saved_case_set_kind: "single_turn_list"
}
: null
};
+52 -8
View File
@@ -128,14 +128,23 @@ function splitQuestionCandidate(raw) {
}
return normalizeRuntimeQuestionList(chunks);
}
function normalizeRuntimeQuestions(value) {
function normalizeRuntimeQuestions(value, options) {
const raw = toArray(value)
.map((item) => (typeof item === "string" ? item.trim() : ""))
.filter((item) => item.length > 0);
if (raw.length === 0) {
return [];
}
const expanded = normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)));
const splitCandidates = options?.splitCandidates ?? true;
const expanded = splitCandidates
? normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)))
: raw
.map((item) => normalizeQuestionChunk(item))
.filter((item) => Boolean(item));
const dedupe = options?.dedupe ?? true;
if (!dedupe) {
return expanded;
}
const deduped = [];
const seen = new Set();
for (const item of expanded) {
@@ -272,6 +281,37 @@ function writeRuntimeAssistantSuiteFromQuestions(jobId, questions) {
fs_1.default.writeFileSync(path_1.default.resolve(config_1.EVAL_CASES_DIR, fileName), JSON.stringify(payload, null, 2), "utf-8");
return fileName;
}
function writeRuntimeAssistantScenarioSuiteFromQuestions(jobId, questions, title) {
if (!fs_1.default.existsSync(config_1.EVAL_CASES_DIR)) {
fs_1.default.mkdirSync(config_1.EVAL_CASES_DIR, { recursive: true });
}
const turns = questions.map((question) => ({
user_message: question
}));
const payload = {
suite_id: `assistant_saved_session_runtime_${jobId}`,
suite_version: "0.1.0",
schema_version: "assistant_saved_session_runtime_v0_1",
title: typeof title === "string" ? title.trim() || null : null,
scenario_count: turns.length > 0 ? 1 : 0,
case_ids: turns.length > 0 ? ["SAVED-001"] : [],
cases: turns.length > 0
? [
{
case_id: "SAVED-001",
scenario_tag: "saved_user_sessions_runtime",
title: typeof title === "string" ? title.trim() || null : null,
question_type: turns.length > 1 ? "followup" : "direct",
broadness_level: "medium",
turns
}
]
: []
};
const fileName = `assistant_saved_session_runtime_${jobId}.json`;
fs_1.default.writeFileSync(path_1.default.resolve(config_1.EVAL_CASES_DIR, fileName), JSON.stringify(payload, null, 2), "utf-8");
return fileName;
}
function readSessionConversation(runId, caseId) {
const sessionId = `${runId}-${caseId}`;
const filePath = path_1.default.resolve(config_1.ASSISTANT_SESSIONS_DIR, `${sessionId}.json`);
@@ -414,15 +454,19 @@ function buildEvalRouter(services) {
throw new http_1.ApiError("UNSUPPORTED_ASYNC_EVAL_TARGET", "Async eval currently supports assistant_stage1 only.", 400);
}
const questions = normalizeRuntimeQuestions(body.questions);
const scenarioQuestions = normalizeRuntimeQuestions(body.scenarioQuestions, { dedupe: false, splitCandidates: false });
const scenarioTitle = toStringSafe(body.scenarioTitle);
const jobId = `job-${(0, nanoid_1.nanoid)(10)}`;
const runId = `assistant-stage1-${(0, nanoid_1.nanoid)(10)}`;
const runtimeCaseSetFile = questions.length > 0
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
: payload.caseSetFile
? payload.caseSetFile
: undefined;
const runtimeCaseSetFile = scenarioQuestions.length > 0
? writeRuntimeAssistantScenarioSuiteFromQuestions(jobId, scenarioQuestions, scenarioTitle ?? undefined)
: questions.length > 0
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
: payload.caseSetFile
? payload.caseSetFile
: undefined;
if (!runtimeCaseSetFile) {
throw new http_1.ApiError("ASYNC_CASESET_REQUIRED", "Async assistant_stage1 run requires caseSetFile or explicit questions[] payload.", 400);
throw new http_1.ApiError("ASYNC_CASESET_REQUIRED", "Async assistant_stage1 run requires caseSetFile, scenarioQuestions[] or explicit questions[] payload.", 400);
}
const caseSeeds = readAssistantSuiteCaseSeeds(runtimeCaseSetFile);
if (caseSeeds.length === 0) {
+1 -1
View File
@@ -64,7 +64,7 @@ function createApp() {
app.use((0, normalize_1.buildNormalizeRouter)(services));
app.use((0, eval_1.buildEvalRouter)(services));
app.use((0, assistant_1.buildAssistantRouter)(services));
app.use((0, autoRuns_1.buildAutoRunsRouter)(openaiClient));
app.use((0, autoRuns_1.buildAutoRunsRouter)(services, openaiClient));
app.use((0, history_1.buildHistoryRouter)());
app.use((0, presets_1.buildPresetsRouter)());
app.use((0, accountingAgent_1.buildAccountingAgentRouter)(services));
+349 -13
View File
@@ -11,13 +11,14 @@ import {
MANUAL_CASE_DECISION_SCHEMA_FILE,
REPORTS_DIR
} from "../config";
import type { AppServices } from "../serverContext";
import { ApiError, ok } from "../utils/http";
import { loadCapabilitiesRegistry, resolveNearestCapabilityGroup, type CapabilityGroup } from "../services/capabilitiesRegistry";
import { OpenAIResponsesClient } from "../services/openaiResponsesClient";
type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
type AutoRunTrend = "up" | "down" | "flat";
type AutoGenMode = "qwen_seed" | "codex_creative";
type AutoGenMode = "qwen_seed" | "codex_creative" | "saved_user_sessions";
type ManualCaseDecision =
| "covered_ok"
| "covered_but_bad_answer"
@@ -175,6 +176,7 @@ interface AutoGenHistoryRecord {
generation_id: string;
created_at: string;
mode: AutoGenMode;
title: string | null;
count: number;
domain: string | null;
questions: string[];
@@ -188,6 +190,9 @@ interface AutoGenHistoryRecord {
prompt_fingerprint: string | null;
autogen_personality_id: string | null;
autogen_personality_prompt: string | null;
source_session_id?: string | null;
saved_session_file?: string | null;
saved_case_set_kind?: string | null;
} | null;
}
@@ -269,6 +274,18 @@ function clampInt(value: number | null, min: number, max: number, fallback: numb
return rounded;
}
function isAutoGenMode(value: unknown): value is AutoGenMode {
return value === "qwen_seed" || value === "codex_creative" || value === "saved_user_sessions";
}
function parseAutoGenTitle(value: unknown): string | null {
const title = toStringSafe(value);
if (!title) {
return null;
}
return title.slice(0, 160);
}
function parseManualCaseDecision(value: unknown, fallback: ManualCaseDecision = "needs_dialog_policy_fix"): ManualCaseDecision {
const normalized = toStringSafe(value);
if (!normalized) return fallback;
@@ -326,15 +343,11 @@ function readAutoGenHistory(): AutoGenHistoryRecord[] {
.map((item) => ({
generation_id: toStringSafe(item.generation_id) ?? "",
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
mode: (toStringSafe(item.mode) as AutoGenMode | null) ?? "codex_creative",
mode: isAutoGenMode(toStringSafe(item.mode)) ? (toStringSafe(item.mode) as AutoGenMode) : "codex_creative",
title: parseAutoGenTitle(item.title),
count: clampInt(toNumberSafe(item.count), 1, 300, 20),
domain: toStringSafe(item.domain),
questions: toArray(item.questions)
.map((q) => toStringSafe(q))
.filter((q): q is string => q !== null)
.map((q) => sanitizeGeneratedQuestion(q))
.filter((q) => q.length > 0)
.slice(0, 500),
questions: parseAssistantSessionQuestions(item.questions),
generated_by: toStringSafe(item.generated_by),
saved_case_set_file: toStringSafe(item.saved_case_set_file),
context: toRecord(item.context)
@@ -349,7 +362,10 @@ function readAutoGenHistory(): AutoGenHistoryRecord[] {
autogen_personality_id: toStringSafe(toRecord(item.context)?.autogen_personality_id),
autogen_personality_prompt: toStringSafe(toRecord(item.context)?.autogen_personality_prompt)
? repairAutogenMojibake(String(toRecord(item.context)?.autogen_personality_prompt))
: null
: null,
source_session_id: toStringSafe(toRecord(item.context)?.source_session_id),
saved_session_file: toStringSafe(toRecord(item.context)?.saved_session_file),
saved_case_set_kind: toStringSafe(toRecord(item.context)?.saved_case_set_kind)
}
: null
}))
@@ -1314,7 +1330,7 @@ function parseDecisionFilter(value: unknown): ManualCaseDecision | "all" {
function parseAutoGenMode(value: unknown): AutoGenMode {
const normalized = toStringSafe(value)?.toLowerCase() ?? "";
if (normalized === "qwen_seed" || normalized === "codex_creative") {
if (normalized === "qwen_seed" || normalized === "codex_creative" || normalized === "saved_user_sessions") {
return normalized;
}
return "codex_creative";
@@ -1416,6 +1432,13 @@ function sanitizeGeneratedQuestion(value: string): string {
.trim();
}
function parseAssistantSessionQuestions(value: unknown): string[] {
return toArray(value)
.map((item) => sanitizeGeneratedQuestion(typeof item === "string" ? item : ""))
.filter((item) => item.length > 0)
.slice(0, 500);
}
const AUTOGEN_QUESTION_PLACEHOLDER_PATTERN = /^(?:questions?|вопросы?|список\s+вопросов)$/iu;
const AUTOGEN_QUESTION_TAIL_PATTERNS: RegExp[] = [
/^(?:без\s+воды|по\s+факту|и\s+коротко|коротко|прям(?:\s+)?сейчас|за\s+весь\s+период|по\s+делу)\??$/iu
@@ -1723,6 +1746,19 @@ function buildAutogenCaseSetFileName(mode: AutoGenMode, generationId: string): s
return `assistant_autogen_${mode}_${stamp}_${generationId}.json`;
}
function buildSavedAssistantSessionSnapshotFileName(generationId: string): string {
const now = new Date();
const stamp = [
now.getUTCFullYear(),
String(now.getUTCMonth() + 1).padStart(2, "0"),
String(now.getUTCDate()).padStart(2, "0"),
String(now.getUTCHours()).padStart(2, "0"),
String(now.getUTCMinutes()).padStart(2, "0"),
String(now.getUTCSeconds()).padStart(2, "0")
].join("");
return `assistant_saved_session_${stamp}_${generationId}.json`;
}
function buildAutogenCaseSetPayload(input: {
generationId: string;
mode: AutoGenMode;
@@ -1757,6 +1793,118 @@ function buildAutogenCaseSetPayload(input: {
};
}
function buildSavedSessionCaseSetPayload(input: {
generationId: string;
title: string | null;
questions: string[];
}): Record<string, unknown> {
const questions = parseAssistantSessionQuestions(input.questions);
const turns = questions.map((question) => ({
user_message: question
}));
const caseId = "SAVED-001";
return {
suite_id: `assistant_saved_session_${input.generationId}`,
suite_version: "0.1.0",
schema_version: "assistant_saved_session_suite_v0_1",
generated_at: new Date().toISOString(),
generation_id: input.generationId,
mode: "saved_user_sessions",
title: input.title,
scenario_count: turns.length > 0 ? 1 : 0,
case_ids: turns.length > 0 ? [caseId] : [],
cases:
turns.length > 0
? [
{
case_id: caseId,
scenario_tag: "saved_user_sessions",
title: input.title,
question_type: turns.length > 1 ? "followup" : "direct",
broadness_level: "medium",
turns
}
]
: []
};
}
function ensureDirSync(targetDir: string): void {
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
}
function writeJsonFile(targetPath: string, payload: unknown): void {
ensureDirSync(path.dirname(targetPath));
fs.writeFileSync(targetPath, JSON.stringify(payload, null, 2), "utf-8");
}
function rewriteAutoGenCaseSetFile(record: AutoGenHistoryRecord): string | null {
const caseSetFile = toStringSafe(record.saved_case_set_file);
if (!caseSetFile) {
return null;
}
const targetPath = path.resolve(EVAL_CASES_DIR, caseSetFile);
const payload =
record.mode === "saved_user_sessions"
? buildSavedSessionCaseSetPayload({
generationId: record.generation_id,
title: record.title,
questions: record.questions
})
: buildAutogenCaseSetPayload({
generationId: record.generation_id,
mode: record.mode,
domain: record.domain,
questions: record.questions
});
writeJsonFile(targetPath, payload);
return caseSetFile;
}
function writeSavedAssistantSessionSnapshot(input: {
generationId: string;
sessionId: string;
title: string | null;
session: Record<string, unknown>;
questions: string[];
}): string {
const fileName = buildSavedAssistantSessionSnapshotFileName(input.generationId);
const targetPath = path.resolve(path.dirname(AUTORUN_GENERATOR_HISTORY_FILE), "saved_sessions", fileName);
writeJsonFile(targetPath, {
saved_at: new Date().toISOString(),
generation_id: input.generationId,
mode: "saved_user_sessions",
title: input.title,
source_session_id: input.sessionId,
questions: input.questions,
session: input.session
});
return fileName;
}
function resolveFileInsideDir(baseDir: string, fileName: string | null | undefined): string | null {
const normalized = toStringSafe(fileName);
if (!normalized) {
return null;
}
const targetPath = path.resolve(baseDir, normalized);
const relative = path.relative(baseDir, targetPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
return null;
}
return targetPath;
}
function safeDeleteFile(targetPath: string | null): string | null {
if (!targetPath || !fs.existsSync(targetPath)) {
return null;
}
fs.unlinkSync(targetPath);
return targetPath;
}
function collectPostAnalysis(
annotations: AutoRunAnnotationRecord[],
runMap: Map<string, IndexedRun>,
@@ -1854,7 +2002,7 @@ function collectPostAnalysis(
};
}
export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()): Router {
export function buildAutoRunsRouter(services: AppServices, openaiClient = new OpenAIResponsesClient()): Router {
const router = Router();
router.get("/api/autoruns/history", (req, res) => {
@@ -2251,7 +2399,7 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
try {
const limit = clampInt(toNumberSafe((req.query as Record<string, unknown>).limit), 1, 500, 120);
const rawMode = toStringSafe((req.query as Record<string, unknown>).mode);
const includeAllModes = !rawMode || !["qwen_seed", "codex_creative"].includes(rawMode);
const includeAllModes = !rawMode || !isAutoGenMode(rawMode);
const modeFilter = (rawMode as AutoGenMode | null) ?? "codex_creative";
const items = readAutoGenHistory()
.filter((item) => (includeAllModes ? true : item.mode === modeFilter))
@@ -2279,6 +2427,182 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
}
});
router.post("/api/autoruns/autogen/save-assistant-session", (req, res, next) => {
try {
const body = toRecord(req.body);
if (!body) {
throw new ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "JSON body is required", 400);
}
const sessionId = toStringSafe(body.session_id);
const title = parseAutoGenTitle(body.title);
const generatedBy = parseAnnotationAuthor(body.generated_by);
const context = toRecord(body.context);
if (!sessionId) {
throw new ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "session_id is required", 400);
}
if (!title) {
throw new ApiError("INVALID_AUTOGEN_SAVE_SESSION_PAYLOAD", "title is required", 400);
}
const session = services.assistantService.getSession(sessionId);
if (!session) {
throw new ApiError("ASSISTANT_SESSION_NOT_FOUND", `Session not found: ${sessionId}`, 404);
}
const questions = session.items
.filter((item: { role: string }) => item.role === "user")
.map((item: { text: string }) => sanitizeGeneratedQuestion(item.text))
.filter((item: string) => item.length > 0);
if (questions.length === 0) {
throw new ApiError("ASSISTANT_SESSION_EMPTY", "Assistant session has no user questions to save.", 400);
}
const generationId = generateAutogenId();
const caseSetFile = buildAutogenCaseSetFileName("saved_user_sessions", generationId);
const caseSetPath = path.resolve(EVAL_CASES_DIR, caseSetFile);
writeJsonFile(
caseSetPath,
buildSavedSessionCaseSetPayload({
generationId,
title,
questions
})
);
const snapshotFile = writeSavedAssistantSessionSnapshot({
generationId,
sessionId,
title,
session: session as unknown as Record<string, unknown>,
questions
});
const record: AutoGenHistoryRecord = {
generation_id: generationId,
created_at: new Date().toISOString(),
mode: "saved_user_sessions",
title,
count: questions.length,
domain: null,
questions,
generated_by: generatedBy,
saved_case_set_file: caseSetFile,
context: {
llm_provider: toStringSafe(context?.llm_provider),
model: toStringSafe(context?.model),
assistant_prompt_version: toStringSafe(context?.assistant_prompt_version),
decomposition_prompt_version: toStringSafe(context?.decomposition_prompt_version),
prompt_fingerprint: toStringSafe(context?.prompt_fingerprint)
? repairAutogenMojibake(String(context?.prompt_fingerprint))
: null,
autogen_personality_id: null,
autogen_personality_prompt: null,
source_session_id: sessionId,
saved_session_file: snapshotFile,
saved_case_set_kind: "assistant_session_scenario"
}
};
const history = readAutoGenHistory();
history.unshift(record);
writeAutoGenHistory(history.slice(0, 500));
ok(res, {
ok: true,
generation: record
});
} catch (error) {
next(error);
}
});
router.patch("/api/autoruns/autogen/history/:generation_id/questions", (req, res, next) => {
try {
const generationId = toStringSafe(req.params.generation_id);
const body = toRecord(req.body);
if (!generationId) {
throw new ApiError("INVALID_AUTOGEN_GENERATION_ID", "generation_id is required", 400);
}
if (!body) {
throw new ApiError("INVALID_AUTOGEN_QUESTIONS_PAYLOAD", "JSON body is required", 400);
}
const questions = parseAssistantSessionQuestions(body.questions);
if (questions.length === 0) {
throw new ApiError("INVALID_AUTOGEN_QUESTIONS_PAYLOAD", "questions must contain at least one item", 400);
}
const history = readAutoGenHistory();
const targetIndex = history.findIndex((item) => item.generation_id === generationId);
if (targetIndex < 0) {
throw new ApiError("AUTOGEN_GENERATION_NOT_FOUND", `Generation not found: ${generationId}`, 404);
}
const current = history[targetIndex];
const updated: AutoGenHistoryRecord = {
...current,
count: questions.length,
questions
};
rewriteAutoGenCaseSetFile(updated);
history[targetIndex] = updated;
writeAutoGenHistory(history);
ok(res, {
ok: true,
generation: updated
});
} catch (error) {
next(error);
}
});
router.delete("/api/autoruns/autogen/history/:generation_id", (req, res, next) => {
try {
const generationId = toStringSafe(req.params.generation_id);
if (!generationId) {
throw new ApiError("INVALID_AUTOGEN_GENERATION_ID", "generation_id is required", 400);
}
const history = readAutoGenHistory();
const targetIndex = history.findIndex((item) => item.generation_id === generationId);
if (targetIndex < 0) {
throw new ApiError("AUTOGEN_GENERATION_NOT_FOUND", `Generation not found: ${generationId}`, 404);
}
const target = history[targetIndex];
const deletedFiles: string[] = [];
const caseSetPath = resolveFileInsideDir(EVAL_CASES_DIR, target.saved_case_set_file);
const savedSessionPath = resolveFileInsideDir(
path.resolve(path.dirname(AUTORUN_GENERATOR_HISTORY_FILE), "saved_sessions"),
target.context?.saved_session_file ?? null
);
const deletedCaseSet = safeDeleteFile(caseSetPath);
if (deletedCaseSet) {
deletedFiles.push(deletedCaseSet);
}
const deletedSavedSession = safeDeleteFile(savedSessionPath);
if (deletedSavedSession) {
deletedFiles.push(deletedSavedSession);
}
history.splice(targetIndex, 1);
writeAutoGenHistory(history);
ok(res, {
ok: true,
generation_id: generationId,
deleted_files: deletedFiles
});
} catch (error) {
next(error);
}
});
router.post("/api/autoruns/autogen/generate", async (req, res, next) => {
try {
const body = toRecord(req.body);
@@ -2294,6 +2618,14 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
const llmConfig = parseAutogenLlmRuntimeConfig(body, context);
const personalityPrompt = toStringSafe(context?.autogen_personality_prompt);
if (mode === "saved_user_sessions") {
throw new ApiError(
"AUTOGEN_MODE_NOT_SUPPORTED",
"Use `/api/autoruns/autogen/save-assistant-session` to save user sessions.",
400
);
}
let questions: string[] = [];
if (mode === "qwen_seed") {
if (!llmConfig) {
@@ -2340,6 +2672,7 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
generation_id: generationId,
created_at: new Date().toISOString(),
mode,
title: null,
count: questions.length,
domain,
questions,
@@ -2357,7 +2690,10 @@ export function buildAutoRunsRouter(openaiClient = new OpenAIResponsesClient()):
autogen_personality_id: toStringSafe(context.autogen_personality_id),
autogen_personality_prompt: toStringSafe(context.autogen_personality_prompt)
? repairAutogenMojibake(String(context.autogen_personality_prompt))
: null
: null,
source_session_id: null,
saved_session_file: null,
saved_case_set_kind: "single_turn_list"
}
: null
};
+50 -4
View File
@@ -176,7 +176,7 @@ function splitQuestionCandidate(raw: string): string[] {
return normalizeRuntimeQuestionList(chunks);
}
function normalizeRuntimeQuestions(value: unknown): string[] {
function normalizeRuntimeQuestions(value: unknown, options?: { dedupe?: boolean; splitCandidates?: boolean }): string[] {
const raw = toArray(value)
.map((item) => (typeof item === "string" ? item.trim() : ""))
.filter((item) => item.length > 0);
@@ -184,7 +184,16 @@ function normalizeRuntimeQuestions(value: unknown): string[] {
return [];
}
const expanded = normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)));
const splitCandidates = options?.splitCandidates ?? true;
const expanded = splitCandidates
? normalizeRuntimeQuestionList(raw.flatMap((item) => splitQuestionCandidate(item)))
: raw
.map((item) => normalizeQuestionChunk(item))
.filter((item): item is string => Boolean(item));
const dedupe = options?.dedupe ?? true;
if (!dedupe) {
return expanded;
}
const deduped: string[] = [];
const seen = new Set<string>();
for (const item of expanded) {
@@ -342,6 +351,39 @@ function writeRuntimeAssistantSuiteFromQuestions(jobId: string, questions: strin
return fileName;
}
function writeRuntimeAssistantScenarioSuiteFromQuestions(jobId: string, questions: string[], title?: string): string {
if (!fs.existsSync(EVAL_CASES_DIR)) {
fs.mkdirSync(EVAL_CASES_DIR, { recursive: true });
}
const turns = questions.map((question) => ({
user_message: question
}));
const payload = {
suite_id: `assistant_saved_session_runtime_${jobId}`,
suite_version: "0.1.0",
schema_version: "assistant_saved_session_runtime_v0_1",
title: typeof title === "string" ? title.trim() || null : null,
scenario_count: turns.length > 0 ? 1 : 0,
case_ids: turns.length > 0 ? ["SAVED-001"] : [],
cases:
turns.length > 0
? [
{
case_id: "SAVED-001",
scenario_tag: "saved_user_sessions_runtime",
title: typeof title === "string" ? title.trim() || null : null,
question_type: turns.length > 1 ? "followup" : "direct",
broadness_level: "medium",
turns
}
]
: []
};
const fileName = `assistant_saved_session_runtime_${jobId}.json`;
fs.writeFileSync(path.resolve(EVAL_CASES_DIR, fileName), JSON.stringify(payload, null, 2), "utf-8");
return fileName;
}
function readSessionConversation(runId: string, caseId: string): EvalAsyncCaseInfo["messages"] {
const sessionId = `${runId}-${caseId}`;
const filePath = path.resolve(ASSISTANT_SESSIONS_DIR, `${sessionId}.json`);
@@ -489,11 +531,15 @@ export function buildEvalRouter(services: AppServices): Router {
throw new ApiError("UNSUPPORTED_ASYNC_EVAL_TARGET", "Async eval currently supports assistant_stage1 only.", 400);
}
const questions = normalizeRuntimeQuestions(body.questions);
const scenarioQuestions = normalizeRuntimeQuestions(body.scenarioQuestions, { dedupe: false, splitCandidates: false });
const scenarioTitle = toStringSafe(body.scenarioTitle);
const jobId = `job-${nanoid(10)}`;
const runId = `assistant-stage1-${nanoid(10)}`;
const runtimeCaseSetFile =
questions.length > 0
scenarioQuestions.length > 0
? writeRuntimeAssistantScenarioSuiteFromQuestions(jobId, scenarioQuestions, scenarioTitle ?? undefined)
: questions.length > 0
? writeRuntimeAssistantSuiteFromQuestions(jobId, questions)
: payload.caseSetFile
? payload.caseSetFile
@@ -502,7 +548,7 @@ export function buildEvalRouter(services: AppServices): Router {
if (!runtimeCaseSetFile) {
throw new ApiError(
"ASYNC_CASESET_REQUIRED",
"Async assistant_stage1 run requires caseSetFile or explicit questions[] payload.",
"Async assistant_stage1 run requires caseSetFile, scenarioQuestions[] or explicit questions[] payload.",
400
);
}
+1 -1
View File
@@ -76,7 +76,7 @@ export function createApp(): express.Express {
app.use(buildNormalizeRouter(services));
app.use(buildEvalRouter(services));
app.use(buildAssistantRouter(services));
app.use(buildAutoRunsRouter(openaiClient));
app.use(buildAutoRunsRouter(services, openaiClient));
app.use(buildHistoryRouter());
app.use(buildPresetsRouter());
app.use(buildAccountingAgentRouter(services));
File diff suppressed because one or more lines are too long
@@ -0,0 +1,215 @@
{
"saved_at": "2026-04-16T17:51:50.474Z",
"generation_id": "gen-mo1s0m9z-ndf56a3",
"mode": "saved_user_sessions",
"title": "Ручная сессия 16.04.2026, 20:51:30",
"source_session_id": "asst-iIpNheKZpP",
"questions": [
"привет как дела"
],
"session": {
"session_id": "asst-iIpNheKZpP",
"updated_at": "2026-04-16T17:51:30.711Z",
"items": [
{
"message_id": "msg-R2if3yso9x",
"session_id": "asst-iIpNheKZpP",
"role": "user",
"text": "привет как дела",
"reply_type": null,
"created_at": "2026-04-16T17:51:19.402Z",
"trace_id": null,
"debug": null
},
{
"message_id": "msg-tRiU0RadHy",
"session_id": "asst-iIpNheKZpP",
"role": "assistant",
"text": "Привет! У меня всё хорошо, спасибо за вопрос. А у тебя как дела? 😊",
"reply_type": "factual_with_explanation",
"created_at": "2026-04-16T17:51:30.711Z",
"trace_id": "chat-2RCiQ9Whq9",
"debug": {
"trace_id": "chat-2RCiQ9Whq9",
"prompt_version": "living_chat_router_v1",
"schema_version": "living_chat_router_v1",
"fallback_type": "none",
"detected_mode": "chat",
"detected_mode_confidence": "high",
"execution_lane": "living_chat",
"living_router_mode": "chat",
"living_router_reason": "non_domain_query_indexed",
"living_chat_response_source": "llm_chat",
"living_chat_script_guard_applied": false,
"living_chat_script_guard_reason": null,
"living_chat_grounding_guard_applied": false,
"living_chat_grounding_guard_reason": null,
"living_chat_data_scope_probe_status": null,
"living_chat_data_scope_probe_channel": null,
"living_chat_data_scope_probe_org_count": 0,
"living_chat_data_scope_probe_organizations": [],
"living_chat_data_scope_probe_error": null,
"living_chat_selected_organization": null,
"assistant_known_organizations": [],
"assistant_active_organization": null,
"address_llm_predecompose_attempted": true,
"address_llm_predecompose_applied": false,
"address_llm_predecompose_reason": "no_usable_fragment",
"address_llm_predecompose_contract": {
"schema_version": "address_llm_predecompose_contract_v1",
"source_message": "привет как дела",
"canonical_message": "привет как дела",
"mode": "unsupported",
"mode_confidence": "low",
"query_shape": "UNKNOWN",
"query_shape_confidence": "low",
"intent": "unknown",
"intent_confidence": "low",
"entities": {
"account": null,
"counterparty": null,
"contract": null,
"document_type": null,
"document_ref": null,
"organization": null
},
"period": {
"scope": "unspecified",
"period_from": null,
"period_to": null,
"as_of_date": null,
"has_explicit_period": false
},
"semantics": {
"scope_kind": "none",
"anchor_kind": "none",
"anchor_value": null,
"date_scope_kind": "none",
"date_basis_hint": null,
"self_scope_detected": false,
"selected_object_scope_detected": false
},
"aggregation_profile": "unknown"
},
"address_semantic_extraction_contract": {
"schema_version": "address_semantic_extraction_contract_v1",
"source_message": "привет как дела",
"canonical_message": "привет как дела",
"canonical_rewrite_applied": false,
"extraction": {
"mode": "unsupported",
"mode_confidence": "low",
"query_shape": "UNKNOWN",
"query_shape_confidence": "low",
"intent": "unknown",
"intent_confidence": "low",
"aggregation_profile": "unknown"
},
"entities": {
"account": null,
"counterparty": null,
"contract": null,
"document_type": null,
"document_ref": null,
"organization": null
},
"period": {
"scope": "unspecified",
"period_from": null,
"period_to": null,
"as_of_date": null,
"has_explicit_period": false
},
"semantics": {
"scope_kind": "none",
"anchor_kind": "none",
"anchor_value": null,
"date_scope_kind": "none",
"date_basis_hint": null,
"self_scope_detected": false,
"selected_object_scope_detected": false
},
"guard_hints": {
"source_data_signal_detected": false,
"canonical_data_signal_detected": false,
"data_scope_meta_query_detected": false,
"deep_investigation_signal_detected": false,
"required_anchor_missing": false,
"unsupported_low_confidence": true,
"semantic_drift_suspected": false
},
"quality": "low",
"valid": false,
"apply_canonical_recommended": false,
"reason_codes": [
"unsupported_low_confidence_contract"
]
},
"orchestration_contract_v1": {
"schema_version": "assistant_orchestration_contract_v1",
"hard_meta_mode": "non_domain",
"address_mode": "unsupported",
"address_mode_confidence": "low",
"address_intent": "unknown",
"address_intent_confidence": "low",
"strong_data_signal_detected": false,
"data_retrieval_signal_detected": false,
"followup_context_detected": false,
"unsupported_address_intent_fallback_to_deep": false,
"final_decision": {
"run_address_lane": false,
"tool_gate_decision": "skip_address_lane",
"tool_gate_reason": "non_domain_query_indexed",
"living_mode": "chat",
"living_reason": "non_domain_query_indexed"
}
},
"tool_gate_decision": "skip_address_lane",
"tool_gate_reason": "non_domain_query_indexed",
"normalized": null,
"normalizer_output": null
}
}
],
"investigation_state": {
"schema_version": "investigation_state_v1",
"session_id": "asst-iIpNheKZpP",
"status": "idle",
"turn_index": 0,
"updated_at": "2026-04-16T17:51:19.401Z",
"question_id": null,
"question_scope_id": null,
"scope_origin": null,
"focus": {
"domain": null,
"period": null,
"primary_accounts": [],
"active_query_subject": null
},
"narrowing_status": "unknown",
"evidence_refs": [],
"open_uncertainties": [],
"last_answer_mode": null,
"followup_context": null,
"query_mode_hint": "direct_answer"
},
"address_navigation_state": {
"schema_version": "address_navigation_state_v1",
"session_id": "asst-iIpNheKZpP",
"updated_at": "2026-04-16T17:51:19.401Z",
"session_context": {
"active_result_set_id": null,
"active_focus_object": null,
"last_confirmed_route": null,
"date_scope": {
"as_of_date": null,
"period_from": null,
"period_to": null
},
"organization_scope": null
},
"result_sets": [],
"navigation_history": []
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
{
"suite_id": "assistant_saved_session_gen-mo1s0m9z-ndf56a3",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-16T17:51:50.471Z",
"generation_id": "gen-mo1s0m9z-ndf56a3",
"mode": "saved_user_sessions",
"title": "Ручная сессия 16.04.2026, 20:51:30",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions",
"title": "Ручная сессия 16.04.2026, 20:51:30",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "привет как дела"
}
]
}
]
}
@@ -0,0 +1,24 @@
{
"suite_id": "assistant_saved_session_runtime_job-qvlcP-qH8S",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "Ручная сессия 16.04.2026, 20:51:30",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "Ручная сессия 16.04.2026, 20:51:30",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "привет как дела"
}
]
}
]
}
@@ -1,6 +1,6 @@
{
"schema_version": "shared_llm_connection_v1",
"updated_at": "2026-04-15T06:12:46.714Z",
"updated_at": "2026-04-16T17:54:57.636Z",
"connection": {
"llmProvider": "local",
"model": "unsloth/qwen3-30b-a3b-instruct-2507",
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-VJV2AL7G.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CaUiKcE3.css">
<script type="module" crossorigin src="/assets/index-Bw40I8e3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DkWsdP2H.css">
</head>
<body>
<div id="root"></div>
+75 -777
View File
@@ -1,22 +1,16 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { apiClient } from "./api/client";
import { AssistantSamPanel } from "./components/AssistantSamPanel";
import { AutoRunsHistoryPanel } from "./components/AutoRunsHistoryPanel";
import { AssistantPanel } from "./components/AssistantPanel";
import { ConnectionPanel } from "./components/ConnectionPanel";
import { HistoryPanel } from "./components/HistoryPanel";
import { MetricsPanel } from "./components/MetricsPanel";
import { OutputPanel } from "./components/OutputPanel";
import { PanelFrame } from "./components/PanelFrame";
import { PromptPanel } from "./components/PromptPanel";
import { QueryPanel } from "./components/QueryPanel";
import { RuntimePanel } from "./components/RuntimePanel";
import { DEFAULT_CONNECTION, DEFAULT_PROMPTS, DEFAULT_QUERY } from "./state/defaults";
import { designConfig } from "../../../designconfig";
import type {
AssistantConversationItem,
AssistantAnnotationRecord,
AssistantSelectionChip,
ConnectionState,
HistoryItem,
NormalizeResultState,
@@ -30,22 +24,10 @@ import type {
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
const AUTORUNS_LAYOUT_CONFIG_KEY = "ndc_autoruns_layout_config_v1";
const AUTORUNS_SAVE_EVENT = "ndc-autoruns-save";
const ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"];
const DEFAULT_UI_MODE: UiMode = "autoruns";
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
const TAB_KEYS: TabKey[] = ["normalized", "fragments", "scope", "flags", "route", "raw", "validation", "logs"];
const DEFAULT_ASSISTANT_ANNOTATION_AUTHOR = "manual_reviewer";
interface AssistantCommentModalState {
open: boolean;
messageIndex: number;
rating: number;
comment: string;
annotationAuthor: string;
saving: boolean;
error: string;
}
function withTs(message: string): string {
return `[${new Date().toLocaleTimeString("ru-RU")}] ${message}`;
@@ -61,25 +43,6 @@ function diffPrompts(current: PromptState, previous: PromptState | null): string
return `Changed fields: ${changed.length}. ${changed.join(" | ")}`;
}
function buildAssistantFollowupMessage(inputValue: string, selectedChip: AssistantSelectionChip | null): string {
const trimmedInput = inputValue.trim();
if (!trimmedInput) {
return "";
}
if (!selectedChip) {
return trimmedInput;
}
const normalizedInput = trimmedInput.toLowerCase();
const selectionAnchor = selectedChip.anchor_text.trim();
const normalizedSelection = selectionAnchor.toLowerCase();
if (normalizedSelection && normalizedInput.includes(normalizedSelection)) {
return trimmedInput;
}
return `По выбранному объекту "${selectionAnchor}": ${trimmedInput}`;
}
export default function App() {
const [connection, setConnection] = useState<ConnectionState>(DEFAULT_CONNECTION);
const [prompts, setPrompts] = useState<PromptState>(DEFAULT_PROMPTS);
@@ -123,11 +86,6 @@ export default function App() {
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = useState(true);
const [showAutorunsCommentsMode, setShowAutorunsCommentsMode] = useState(true);
const [showAssistantConnectionMode, setShowAssistantConnectionMode] = useState(true);
const [showAssistantPromptMode, setShowAssistantPromptMode] = useState(true);
const [showAssistantChatMode, setShowAssistantChatMode] = useState(true);
const [showAssistantCommentsMode, setShowAssistantCommentsMode] = useState(true);
const [showAssistantSamMode, setShowAssistantSamMode] = useState(true);
const [showDecompositionConnectionMode, setShowDecompositionConnectionMode] = useState(true);
const [showDecompositionPromptMode, setShowDecompositionPromptMode] = useState(true);
const [showDecompositionQueryMode, setShowDecompositionQueryMode] = useState(true);
@@ -135,24 +93,6 @@ export default function App() {
const [showDecompositionMetricsMode, setShowDecompositionMetricsMode] = useState(true);
const [showDecompositionHistoryMode, setShowDecompositionHistoryMode] = useState(true);
const [showDecompositionRuntimeMode, setShowDecompositionRuntimeMode] = useState(true);
const [assistantSessionId, setAssistantSessionId] = useState("");
const [assistantConversation, setAssistantConversation] = useState<AssistantConversationItem[]>([]);
const [assistantInput, setAssistantInput] = useState("");
const [assistantSelectedChip, setAssistantSelectedChip] = useState<AssistantSelectionChip | null>(null);
const [assistantBusy, setAssistantBusy] = useState(false);
const [assistantStatus, setAssistantStatus] = useState("");
const [assistantError, setAssistantError] = useState("");
const [assistantAnnotations, setAssistantAnnotations] = useState<AssistantAnnotationRecord[]>([]);
const [assistantAnnotationsBusy, setAssistantAnnotationsBusy] = useState(false);
const [assistantCommentModal, setAssistantCommentModal] = useState<AssistantCommentModalState>({
open: false,
messageIndex: -1,
rating: 3,
comment: "",
annotationAuthor: DEFAULT_ASSISTANT_ANNOTATION_AUTHOR,
saving: false,
error: ""
});
const presetAutoloadDoneRef = useRef(false);
const skipPresetAutoloadRef = useRef(false);
const sharedConnectionSyncReadyRef = useRef(false);
@@ -184,16 +124,6 @@ export default function App() {
setAppLogs((prev) => [withTs(message), ...prev].slice(0, 300));
};
function startAssistantStatusTicker(): () => void {
let index = 0;
setAssistantStatus(ASSISTANT_STAGES[0]);
const timer = window.setInterval(() => {
index = Math.min(index + 1, ASSISTANT_STAGES.length - 1);
setAssistantStatus(ASSISTANT_STAGES[index]);
}, 650);
return () => window.clearInterval(timer);
}
useEffect(() => {
const bootstrapSharedConnection = async () => {
const cached = localStorage.getItem(SESSION_CONFIG_KEY);
@@ -239,7 +169,7 @@ export default function App() {
if (cachedAutorunsLayout) {
try {
const parsed = JSON.parse(cachedAutorunsLayout) as {
uiMode?: UiMode;
uiMode?: UiMode | "assistant";
activeTab?: TabKey;
showAutorunsSettingsMode?: boolean;
showAutorunsAutoRunsMode?: boolean;
@@ -247,11 +177,6 @@ export default function App() {
showAutorunsDecompositionMode?: boolean;
showAutorunsProgressMode?: boolean;
showAutorunsCommentsMode?: boolean;
showAssistantConnectionMode?: boolean;
showAssistantPromptMode?: boolean;
showAssistantChatMode?: boolean;
showAssistantCommentsMode?: boolean;
showAssistantSamMode?: boolean;
showDecompositionConnectionMode?: boolean;
showDecompositionPromptMode?: boolean;
showDecompositionQueryMode?: boolean;
@@ -261,9 +186,7 @@ export default function App() {
showDecompositionRuntimeMode?: boolean;
prompts?: PromptState;
};
if (parsed.uiMode === "decomposition") {
setUiMode("decomposition");
} else if (parsed.uiMode === "assistant" || parsed.uiMode === "autoruns") {
if (parsed.uiMode === "assistant" || parsed.uiMode === "autoruns" || parsed.uiMode === "decomposition") {
setUiMode("autoruns");
}
if (parsed.activeTab && TAB_KEYS.includes(parsed.activeTab)) {
@@ -287,21 +210,6 @@ export default function App() {
if (typeof parsed.showAutorunsCommentsMode === "boolean") {
setShowAutorunsCommentsMode(parsed.showAutorunsCommentsMode);
}
if (typeof parsed.showAssistantConnectionMode === "boolean") {
setShowAssistantConnectionMode(parsed.showAssistantConnectionMode);
}
if (typeof parsed.showAssistantPromptMode === "boolean") {
setShowAssistantPromptMode(parsed.showAssistantPromptMode);
}
if (typeof parsed.showAssistantChatMode === "boolean") {
setShowAssistantChatMode(parsed.showAssistantChatMode);
}
if (typeof parsed.showAssistantCommentsMode === "boolean") {
setShowAssistantCommentsMode(parsed.showAssistantCommentsMode);
}
if (typeof parsed.showAssistantSamMode === "boolean") {
setShowAssistantSamMode(parsed.showAssistantSamMode);
}
if (typeof parsed.showDecompositionConnectionMode === "boolean") {
setShowDecompositionConnectionMode(parsed.showDecompositionConnectionMode);
}
@@ -448,11 +356,6 @@ export default function App() {
showAutorunsDecompositionMode,
showAutorunsProgressMode,
showAutorunsCommentsMode,
showAssistantConnectionMode,
showAssistantPromptMode,
showAssistantChatMode,
showAssistantCommentsMode,
showAssistantSamMode,
showDecompositionConnectionMode,
showDecompositionPromptMode,
showDecompositionQueryMode,
@@ -740,207 +643,6 @@ export default function App() {
}
}
const assistantAnnotationsByMessageId = useMemo(() => {
const map = new Map<string, AssistantAnnotationRecord>();
for (const item of assistantAnnotations) {
if (item.message_id) {
map.set(item.message_id, item);
}
}
return map;
}, [assistantAnnotations]);
const assistantCommentModalMessage =
assistantCommentModal.messageIndex >= 0 ? assistantConversation[assistantCommentModal.messageIndex] ?? null : null;
const assistantCommentModalQuestion = useMemo(() => {
if (assistantCommentModal.messageIndex < 0) return null;
for (let index = assistantCommentModal.messageIndex - 1; index >= 0; index -= 1) {
const candidate = assistantConversation[index];
if (candidate?.role === "user") {
return candidate;
}
}
return null;
}, [assistantCommentModal.messageIndex, assistantConversation]);
async function loadAssistantAnnotationsForSession(sessionId: string): Promise<void> {
if (!sessionId.trim()) {
setAssistantAnnotations([]);
return;
}
setAssistantAnnotationsBusy(true);
try {
const payload = await apiClient.loadAssistantAnnotations({
session_id: sessionId,
limit: 400
});
setAssistantAnnotations(payload.items ?? []);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log(`Assistant annotations load error: ${message}`);
} finally {
setAssistantAnnotationsBusy(false);
}
}
function closeAssistantCommentModal(options?: { force?: boolean }) {
setAssistantCommentModal((prev) => {
if (prev.saving && !options?.force) {
return prev;
}
return {
open: false,
messageIndex: -1,
rating: 3,
comment: "",
annotationAuthor: DEFAULT_ASSISTANT_ANNOTATION_AUTHOR,
saving: false,
error: ""
};
});
}
function openAssistantCommentModal(item: AssistantConversationItem, index: number): void {
if (item.role !== "assistant") return;
const sessionIdFromState = assistantSessionId.trim();
const sessionIdFromItem = String(item.session_id ?? "").trim();
const resolvedSessionId = sessionIdFromState || sessionIdFromItem;
if (!resolvedSessionId) {
setAssistantError("Сначала получите ответ ассистента в активной сессии.");
return;
}
if (!sessionIdFromState && sessionIdFromItem) {
setAssistantSessionId(sessionIdFromItem);
}
const existing = assistantAnnotationsByMessageId.get(item.message_id) ?? null;
setAssistantCommentModal({
open: true,
messageIndex: index,
rating: existing?.rating ?? 3,
comment: existing?.comment ?? "",
annotationAuthor: existing?.annotation_author ?? DEFAULT_ASSISTANT_ANNOTATION_AUTHOR,
saving: false,
error: ""
});
}
function canCommentAssistantMessage(item: AssistantConversationItem): boolean {
return item.role === "assistant";
}
function isAssistantMessageCommented(item: AssistantConversationItem): boolean {
return item.role === "assistant" && assistantAnnotationsByMessageId.has(item.message_id);
}
async function submitAssistantCommentModal(): Promise<void> {
if (!assistantSessionId.trim()) {
setAssistantCommentModal((prev) => ({ ...prev, error: "Сессия ассистента не найдена." }));
return;
}
if (assistantCommentModal.messageIndex < 0) {
return;
}
if (!assistantCommentModal.comment.trim()) {
setAssistantCommentModal((prev) => ({ ...prev, error: "Добавьте комментарий." }));
return;
}
setAssistantCommentModal((prev) => ({ ...prev, saving: true, error: "" }));
try {
const payload = await apiClient.saveAssistantAnnotation({
session_id: assistantSessionId,
message_index: assistantCommentModal.messageIndex,
rating: assistantCommentModal.rating,
comment: assistantCommentModal.comment.trim(),
annotation_author: assistantCommentModal.annotationAuthor.trim() || undefined
});
setAssistantAnnotations((prev) => {
const next = [...prev];
const index = next.findIndex((item) => item.annotation_id === payload.annotation.annotation_id);
if (index >= 0) {
next[index] = payload.annotation;
} else {
next.unshift(payload.annotation);
}
return next.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
});
closeAssistantCommentModal({ force: true });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssistantCommentModal((prev) => ({ ...prev, saving: false, error: message }));
}
}
function resetAssistantSession() {
setAssistantSessionId("");
setAssistantConversation([]);
setAssistantInput("");
setAssistantSelectedChip(null);
setAssistantStatus("");
setAssistantError("");
setAssistantAnnotations([]);
closeAssistantCommentModal({ force: true });
log("Assistant session reset.");
}
async function sendAssistantMessage() {
const userMessage = buildAssistantFollowupMessage(assistantInput, assistantSelectedChip);
if (!userMessage) {
return;
}
setAssistantBusy(true);
setAssistantError("");
setAssistantInput("");
setAssistantConversation((prev) => [
...prev,
{
message_id: `local-${Date.now()}`,
session_id: assistantSessionId || "pending",
role: "user",
text: userMessage,
reply_type: null,
created_at: new Date().toISOString(),
trace_id: null,
debug: null
}
]);
const stopTicker = startAssistantStatusTicker();
try {
const response = await apiClient.sendAssistantMessage({
connection,
prompts,
userMessage,
sessionId: assistantSessionId || undefined,
promptVersion: ASSISTANT_PROMPT_VERSION,
useMock
});
setAssistantSessionId(response.session_id);
setAssistantConversation(response.conversation);
setAssistantStatus("Ответ готов");
await loadAssistantAnnotationsForSession(response.session_id);
log(`Assistant reply received: trace=${response.debug.trace_id}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAssistantError(message);
setAssistantStatus("Ошибка ассистента");
log(`Assistant error: ${message}`);
} finally {
stopTicker();
setAssistantBusy(false);
}
}
useEffect(() => {
if (!assistantSessionId.trim()) {
setAssistantAnnotations([]);
return;
}
void loadAssistantAnnotationsForSession(assistantSessionId);
}, [assistantSessionId]);
useEffect(() => {
if (!selectedRunId) {
setRunTrace([]);
@@ -953,493 +655,89 @@ export default function App() {
}, [selectedRunId]);
return (
<main
className={`app-root ${
uiMode === "assistant" || uiMode === "decomposition" || uiMode === "autoruns" ? "app-root-autoruns" : ""
}`}
>
<main className="app-root app-root-autoruns">
<header className="app-topbar">
<div className="mode-switch-row">
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
<button type="button" className="tab active" onClick={() => setUiMode("autoruns")}>
Управление ассистентом
</button>
<button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
Декомпозиция
</button>
<button type="button" className="tab" onClick={saveAutorunsLayout}>
Сохранить
</button>
</div>
{uiMode === "assistant" ? (
<div className="mode-switch-row mode-switch-row-right">
<button
type="button"
className={showAssistantConnectionMode ? "tab active" : "tab"}
onClick={() => setShowAssistantConnectionMode((prev) => !prev)}
>
LLM Connector
</button>
<button
type="button"
className={showAssistantPromptMode ? "tab active" : "tab"}
onClick={() => setShowAssistantPromptMode((prev) => !prev)}
>
Prompt Manager
</button>
<button type="button" className={showAssistantChatMode ? "tab active" : "tab"} onClick={() => setShowAssistantChatMode((prev) => !prev)}>
Режим ассистента
</button>
<button
type="button"
className={showAssistantCommentsMode ? "tab active" : "tab"}
onClick={() => setShowAssistantCommentsMode((prev) => !prev)}
>
Комментарии ассистента
</button>
<button type="button" className={showAssistantSamMode ? "tab active" : "tab"} onClick={() => setShowAssistantSamMode((prev) => !prev)}>
SAM
</button>
</div>
) : uiMode === "decomposition" ? (
<div className="mode-switch-row mode-switch-row-right">
<button
type="button"
className={showDecompositionConnectionMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionConnectionMode((prev) => !prev)}
>
LLM
</button>
<button
type="button"
className={showDecompositionPromptMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionPromptMode((prev) => !prev)}
>
Prompt
</button>
<button type="button" className={showDecompositionQueryMode ? "tab active" : "tab"} onClick={() => setShowDecompositionQueryMode((prev) => !prev)}>
Запрос
</button>
<button type="button" className={showDecompositionOutputMode ? "tab active" : "tab"} onClick={() => setShowDecompositionOutputMode((prev) => !prev)}>
Выход
</button>
<button
type="button"
className={showDecompositionMetricsMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionMetricsMode((prev) => !prev)}
>
Метрики
</button>
<button
type="button"
className={showDecompositionHistoryMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionHistoryMode((prev) => !prev)}
>
История
</button>
<button
type="button"
className={showDecompositionRuntimeMode ? "tab active" : "tab"}
onClick={() => setShowDecompositionRuntimeMode((prev) => !prev)}
>
NDC Run Monitor
</button>
</div>
) : uiMode === "autoruns" ? (
<div className="mode-switch-row mode-switch-row-right">
<button
type="button"
className={showAutorunsSettingsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsSettingsMode((prev) => !prev)}
>
Настройки
</button>
<button
type="button"
className={showAutorunsAutoRunsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsAutoRunsMode((prev) => !prev)}
>
Автопрогоны
</button>
<button
type="button"
className={showAutorunsAssistantMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsAssistantMode((prev) => !prev)}
>
Режим ассистента
</button>
<button
type="button"
className={showAutorunsDecompositionMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsDecompositionMode((prev) => !prev)}
>
Режим декомпозиции
</button>
<button
type="button"
className={showAutorunsProgressMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsProgressMode((prev) => !prev)}
>
Прогресс/регресс
</button>
<button
type="button"
className={showAutorunsCommentsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsCommentsMode((prev) => !prev)}
>
Комментарии
</button>
</div>
) : null}
<div className="mode-switch-row mode-switch-row-right">
<button
type="button"
className={showAutorunsSettingsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsSettingsMode((prev) => !prev)}
>
Настройки
</button>
<button
type="button"
className={showAutorunsAutoRunsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsAutoRunsMode((prev) => !prev)}
>
Автопрогоны
</button>
<button
type="button"
className={showAutorunsAssistantMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsAssistantMode((prev) => !prev)}
>
Режим ассистента
</button>
<button
type="button"
className={showAutorunsProgressMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsProgressMode((prev) => !prev)}
>
Прогресс/регресс
</button>
<button
type="button"
className={showAutorunsCommentsMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsCommentsMode((prev) => !prev)}
>
Комментарии
</button>
</div>
</header>
{uiMode === "assistant" ? (
<div className="layout-grid layout-grid-mode-columns">
<div className="mode-columns">
{showAssistantConnectionMode ? (
<div className="mode-col">
<ConnectionPanel
value={connection}
modelOptions={modelOptions}
modelsBusy={modelsBusy}
onChange={setConnection}
onReloadModels={reloadModels}
onSaveLocalConfig={saveLocalConfig}
onTestConnection={testConnection}
lastStatus={connectionStatus}
busy={busy || assistantBusy}
/>
</div>
) : null}
{showAssistantPromptMode ? (
<div className="mode-col mode-col-wide">
<PromptPanel
value={prompts}
onChange={setPrompts}
presets={presetList}
selectedPresetId={selectedPresetId}
onSelectPreset={setSelectedPresetId}
onLoadPreset={loadSelectedPreset}
onSavePreset={savePreset}
onResetDefaults={resetDefaults}
onDiffPrevious={diffWithPrevious}
presetName={presetName}
onPresetNameChange={setPresetName}
diffSummary={diffSummary}
/>
</div>
) : null}
{showAssistantChatMode ? (
<div className="mode-col mode-col-xwide">
<AssistantPanel
sessionId={assistantSessionId}
conversation={assistantConversation}
inputValue={assistantInput}
onInputChange={setAssistantInput}
selectedContextChip={assistantSelectedChip}
onSelectContextChip={setAssistantSelectedChip}
onClearContextChip={() => setAssistantSelectedChip(null)}
useMock={useMock}
onUseMockChange={setUseMock}
onSend={sendAssistantMessage}
onClear={resetAssistantSession}
busy={assistantBusy}
statusText={assistantStatus}
errorMessage={assistantError}
showCommentAction
onCommentAssistantMessage={openAssistantCommentModal}
isAssistantMessageCommented={isAssistantMessageCommented}
canCommentAssistantMessage={canCommentAssistantMessage}
/>
</div>
) : null}
{showAssistantCommentsMode ? (
<div className="mode-col">
<PanelFrame className="assistant-comments-frame" title="Комментарии ассистента">
<div className="assistant-comments-shell">
<div className="assistant-comments-toolbar">
<span className="muted">
{assistantSessionId ? `session: ${assistantSessionId}` : "Сессия не запущена"}
</span>
<button
type="button"
className="tab"
onClick={() => void loadAssistantAnnotationsForSession(assistantSessionId)}
disabled={!assistantSessionId || assistantAnnotationsBusy}
>
{assistantAnnotationsBusy ? "Обновляю..." : "Обновить"}
</button>
</div>
<div className="assistant-comments-list">
{!assistantSessionId ? <p className="muted">Появится после первого ответа ассистента.</p> : null}
{assistantSessionId && assistantAnnotations.length === 0 && !assistantAnnotationsBusy ? (
<p className="muted">Комментариев по этой сессии пока нет.</p>
) : null}
{assistantAnnotations.map((item) => (
<article key={item.annotation_id} className="assistant-comment-item">
<div className="assistant-comment-head">
<strong>{`${"●".repeat(Math.max(1, Math.min(5, Math.round(item.rating))))}${"○".repeat(Math.max(0, 5 - Math.round(item.rating)))}`}</strong>
<span>{new Date(item.updated_at).toLocaleString("ru-RU")}</span>
</div>
{item.context.question_text ? <p>Q: {item.context.question_text}</p> : null}
{item.context.answer_text ? <p>A: {item.context.answer_text}</p> : null}
<p>{item.comment}</p>
<div className="assistant-comment-meta">
{item.context.trace_id ? <span>{`trace=${item.context.trace_id}`}</span> : null}
{item.context.reply_type ? <span>{`reply_type=${item.context.reply_type}`}</span> : null}
</div>
</article>
))}
</div>
</div>
</PanelFrame>
</div>
) : null}
{showAssistantSamMode ? (
<div className="mode-col">
<AssistantSamPanel
sessionId={assistantSessionId}
conversation={assistantConversation}
statusText={assistantStatus}
errorMessage={assistantError}
useMock={useMock}
appLogs={appLogs}
/>
</div>
) : null}
{!showAssistantConnectionMode &&
!showAssistantPromptMode &&
!showAssistantChatMode &&
!showAssistantCommentsMode &&
!showAssistantSamMode ? (
<div className="mode-columns-empty">Все панели режима ассистента скрыты. Включите нужные блоки справа в шапке.</div>
) : null}
</div>
</div>
) : uiMode === "decomposition" ? (
<div className="layout-grid layout-grid-mode-columns">
<div className="mode-columns">
{showDecompositionConnectionMode ? (
<div className="mode-col">
<ConnectionPanel
value={connection}
modelOptions={modelOptions}
modelsBusy={modelsBusy}
onChange={setConnection}
onReloadModels={reloadModels}
onSaveLocalConfig={saveLocalConfig}
onTestConnection={testConnection}
lastStatus={connectionStatus}
busy={busy}
/>
</div>
) : null}
{showDecompositionPromptMode ? (
<div className="mode-col mode-col-wide">
<PromptPanel
value={prompts}
onChange={setPrompts}
presets={presetList}
selectedPresetId={selectedPresetId}
onSelectPreset={setSelectedPresetId}
onLoadPreset={loadSelectedPreset}
onSavePreset={savePreset}
onResetDefaults={resetDefaults}
onDiffPrevious={diffWithPrevious}
presetName={presetName}
onPresetNameChange={setPresetName}
diffSummary={diffSummary}
/>
</div>
) : null}
{showDecompositionQueryMode ? (
<div className="mode-col">
<QueryPanel
value={query}
onChange={setQuery}
onApplyBatchFormat={applyBatchFormat}
onNormalize={normalize}
busy={busy}
useMock={useMock}
onUseMockChange={setUseMock}
errorMessage={lastError}
/>
</div>
) : null}
{showDecompositionOutputMode ? (
<div className="mode-col mode-col-xwide">
<OutputPanel tab={activeTab} onTabChange={setActiveTab} result={result} appLogs={appLogs} />
</div>
) : null}
{showDecompositionMetricsMode ? (
<div className="mode-col">
<MetricsPanel result={result} />
</div>
) : null}
{showDecompositionHistoryMode ? (
<div className="mode-col">
<HistoryPanel items={historyItems} onRefresh={refreshHistory} onOpenTrace={openTrace} />
</div>
) : null}
{showDecompositionRuntimeMode ? (
<div className="mode-col mode-col-xwide">
<RuntimePanel
runs={runs}
selectedRunId={selectedRunId}
onSelectRun={setSelectedRunId}
onStartRun={startRun}
onFinishRun={finishRun}
onRefreshRuns={refreshRuns}
onRunEval={runEval}
onCopyEvalReport={copyEvalReport}
evalBusy={evalBusy}
traceItems={runTrace}
evalReport={evalReport}
/>
</div>
) : null}
{!showDecompositionConnectionMode &&
!showDecompositionPromptMode &&
!showDecompositionQueryMode &&
!showDecompositionOutputMode &&
!showDecompositionMetricsMode &&
!showDecompositionHistoryMode &&
!showDecompositionRuntimeMode ? (
<div className="mode-columns-empty">Все панели режима декомпозиции скрыты. Включите нужные блоки справа в шапке.</div>
) : null}
</div>
</div>
) : (
<div className="layout-grid layout-grid-autoruns">
<AutoRunsHistoryPanel
connection={connection}
modelOptions={modelOptions}
modelsBusy={modelsBusy}
connectionStatus={connectionStatus}
connectionBusy={busy}
onConnectionChange={setConnection}
onReloadModels={reloadModels}
onSaveLocalConfig={saveLocalConfig}
onTestConnection={testConnection}
prompts={prompts}
onPromptsChange={setPrompts}
promptPresets={presetList}
selectedPresetId={selectedPresetId}
onSelectPreset={setSelectedPresetId}
onLoadPreset={loadSelectedPreset}
onSavePreset={savePreset}
onResetDefaults={resetDefaults}
onDiffPrevious={diffWithPrevious}
presetName={presetName}
onPresetNameChange={setPresetName}
diffSummary={diffSummary}
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
showSettingsMode={showAutorunsSettingsMode}
showAutoRunsMode={showAutorunsAutoRunsMode}
showAssistantMode={showAutorunsAssistantMode}
showDecompositionMode={showAutorunsDecompositionMode}
showProgressMode={showAutorunsProgressMode}
showCommentsMode={showAutorunsCommentsMode}
onLog={log}
/>
</div>
)}
{assistantCommentModal.open ? (
<div
className="autoruns-comment-modal-backdrop"
onClick={(event) => {
if (event.target === event.currentTarget) {
closeAssistantCommentModal();
}
}}
>
<div className="autoruns-comment-modal">
<h3>Комментарий к ответу ассистента</h3>
<p className="muted">Эта разметка хранится отдельно от комментариев автопрогонов.</p>
{assistantCommentModalQuestion ? (
<details className="autoruns-prompt-details" open>
<summary>Вопрос пользователя</summary>
<p className="autoruns-comment-quote">{assistantCommentModalQuestion.text}</p>
</details>
) : null}
{assistantCommentModalMessage ? (
<details className="autoruns-prompt-details" open>
<summary>Ответ ассистента</summary>
<p className="autoruns-comment-quote">{assistantCommentModalMessage.text}</p>
</details>
) : null}
<div className="autoruns-rating-row" role="group" aria-label="Рейтинг ответа">
{[1, 2, 3, 4, 5].map((value) => (
<button
key={value}
type="button"
className={assistantCommentModal.rating >= value ? "autoruns-rating-dot active" : "autoruns-rating-dot"}
onClick={() => setAssistantCommentModal((prev) => ({ ...prev, rating: value }))}
disabled={assistantCommentModal.saving}
aria-label={`Оценка ${value}`}
>
{assistantCommentModal.rating >= value ? "●" : "○"}
</button>
))}
</div>
<div className="autoruns-form-grid">
<label>
Автор комментария
<input
value={assistantCommentModal.annotationAuthor}
onChange={(event) => setAssistantCommentModal((prev) => ({ ...prev, annotationAuthor: event.target.value }))}
placeholder="manual_reviewer"
disabled={assistantCommentModal.saving}
/>
</label>
</div>
<label>
Комментарий
<textarea
value={assistantCommentModal.comment}
onChange={(event) => setAssistantCommentModal((prev) => ({ ...prev, comment: event.target.value }))}
placeholder="Что именно не так в ответе и что проверить."
rows={4}
disabled={assistantCommentModal.saving}
/>
</label>
{assistantCommentModal.error ? <p className="error-text">{assistantCommentModal.error}</p> : null}
<div className="button-row">
<button type="button" onClick={() => void submitAssistantCommentModal()} disabled={assistantCommentModal.saving}>
{assistantCommentModal.saving ? "Сохраняю..." : "Готово"}
</button>
<button
type="button"
className="tab"
onClick={() => closeAssistantCommentModal()}
disabled={assistantCommentModal.saving}
>
Отмена
</button>
</div>
</div>
</div>
) : null}
<div className="layout-grid layout-grid-autoruns">
<AutoRunsHistoryPanel
connection={connection}
modelOptions={modelOptions}
modelsBusy={modelsBusy}
connectionStatus={connectionStatus}
connectionBusy={busy}
onConnectionChange={setConnection}
onReloadModels={reloadModels}
onSaveLocalConfig={saveLocalConfig}
onTestConnection={testConnection}
prompts={prompts}
onPromptsChange={setPrompts}
promptPresets={presetList}
selectedPresetId={selectedPresetId}
onSelectPreset={setSelectedPresetId}
onLoadPreset={loadSelectedPreset}
onSavePreset={savePreset}
onResetDefaults={resetDefaults}
onDiffPrevious={diffWithPrevious}
presetName={presetName}
onPresetNameChange={setPresetName}
diffSummary={diffSummary}
assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
showSettingsMode={showAutorunsSettingsMode}
showAutoRunsMode={showAutorunsAutoRunsMode}
showAssistantMode={showAutorunsAssistantMode}
showProgressMode={showAutorunsProgressMode}
showCommentsMode={showAutorunsCommentsMode}
onLog={log}
/>
</div>
</main>
);
}
+46 -1
View File
@@ -3,6 +3,7 @@ import type {
AsyncEvalRunStatusResponse,
AutoGenPersonalityCatalogResponse,
AutoGenHistoryResponse,
AutoGenHistoryRecord,
AutoGenMode,
AutoRunAnnotationsResponse,
AutoRunAnnotationRecord,
@@ -230,6 +231,8 @@ export const apiClient = {
evalTarget?: "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0";
compareWithReportFile?: string;
questions?: string[];
scenarioQuestions?: string[];
scenarioTitle?: string;
analysisDate?: string;
}): Promise<AsyncEvalRunStartResponse> {
return request("/eval/run-async/start", {
@@ -256,6 +259,8 @@ export const apiClient = {
eval_target: input.evalTarget,
compare_with_report_file: input.compareWithReportFile,
questions: input.questions,
scenarioQuestions: input.scenarioQuestions,
scenarioTitle: input.scenarioTitle,
analysis_date: input.analysisDate
})
});
@@ -342,6 +347,24 @@ export const apiClient = {
return request(`/assistant/session/${sessionId}`);
},
async saveAutoRunAssistantSession(input: {
session_id: string;
title: string;
generated_by?: string;
context?: {
llm_provider?: string;
model?: string;
assistant_prompt_version?: string;
decomposition_prompt_version?: string;
prompt_fingerprint?: string;
};
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
return request("/autoruns/autogen/save-assistant-session", {
method: "POST",
body: JSON.stringify(input)
});
},
async loadAssistantAnnotations(input?: {
session_id?: string;
limit?: number;
@@ -485,6 +508,28 @@ export const apiClient = {
return request("/autoruns/autogen/personality-catalog");
},
async updateAutoRunAutogenQuestions(input: {
generation_id: string;
questions: string[];
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
return request(`/autoruns/autogen/history/${encodeURIComponent(input.generation_id)}/questions`, {
method: "PATCH",
body: JSON.stringify({
questions: input.questions
})
});
},
async deleteAutoRunAutogenHistoryRecord(generationId: string): Promise<{
ok: boolean;
generation_id: string;
deleted_files: string[];
}> {
return request(`/autoruns/autogen/history/${encodeURIComponent(generationId)}`, {
method: "DELETE"
});
},
async generateAutoRunQuestions(input: {
mode: AutoGenMode;
count: number;
@@ -508,7 +553,7 @@ export const apiClient = {
autogen_personality_id?: string;
autogen_personality_prompt?: string;
};
}): Promise<{ ok: boolean; generation: { generation_id: string; created_at: string; mode: AutoGenMode; count: number; domain: string | null; questions: string[]; generated_by: string | null; saved_case_set_file: string | null; context: Record<string, unknown> | null } }> {
}): Promise<{ ok: boolean; generation: AutoGenHistoryRecord }> {
return request("/autoruns/autogen/generate", {
method: "POST",
body: JSON.stringify(input)
@@ -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"
+6 -2
View File
@@ -66,11 +66,11 @@ export interface RuntimeRun {
updatedAt: string;
}
export type UiMode = "assistant" | "decomposition" | "autoruns";
export type UiMode = "decomposition" | "autoruns";
export type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
export type AutoRunTrend = "up" | "down" | "flat";
export type AutoGenMode = "qwen_seed" | "codex_creative";
export type AutoGenMode = "qwen_seed" | "codex_creative" | "saved_user_sessions";
export type ManualCaseDecision =
| "covered_ok"
| "covered_but_bad_answer"
@@ -317,6 +317,7 @@ export interface AutoGenHistoryRecord {
generation_id: string;
created_at: string;
mode: AutoGenMode;
title: string | null;
count: number;
domain: string | null;
questions: string[];
@@ -330,6 +331,9 @@ export interface AutoGenHistoryRecord {
prompt_fingerprint: string | null;
autogen_personality_id: string | null;
autogen_personality_prompt: string | null;
source_session_id?: string | null;
saved_session_file?: string | null;
saved_case_set_kind?: string | null;
} | null;
}
+32 -2
View File
@@ -1566,14 +1566,13 @@ button:disabled {
display: inline-flex;
align-items: center;
justify-content: center;
transform: rotate(45deg);
box-shadow: none;
transition: color 0.15s ease;
}
.autoruns-remove-question-btn:hover {
background: transparent;
color: rgb(var(--rgb-active-text));
color: rgb(var(--rgb-active));
box-shadow: none;
}
@@ -1772,6 +1771,37 @@ button:disabled {
background: rgb(var(--rgb-active));
}
.autoruns-autogen-card-actions {
display: inline-flex;
align-items: center;
gap: 8px;
}
.autoruns-autogen-delete-btn {
border: none;
border-radius: 0;
width: 22px;
height: 22px;
min-width: 22px;
padding: 0;
background: transparent;
color: rgb(var(--rgb-text-main));
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 700;
line-height: 1;
box-shadow: none;
transition: color 0.15s ease;
}
.autoruns-autogen-delete-btn:hover {
background: transparent;
color: rgb(var(--rgb-active));
box-shadow: none;
}
@media (max-width: 1200px) {
:root {
--mode-column-width: 400px;