Усилить answer contract и агентный аудит для phase105

This commit is contained in:
2026-05-21 09:00:08 +03:00
parent 9c86407937
commit bbc257fd6c
34 changed files with 1533 additions and 184 deletions
@@ -21,6 +21,7 @@ import type {
ManualCaseDecision,
PromptState
} from "../state/types";
import { buildAutoRunDialogExportForCopy, type ConversationExportMode } from "../utils/conversationExport";
import { AssistantPanel } from "./AssistantPanel";
import { ConnectionPanel } from "./ConnectionPanel";
import { JsonView } from "./JsonView";
@@ -620,6 +621,37 @@ function CardStopIcon() {
);
}
async function writeTextToClipboard(text: string): Promise<boolean> {
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Fall back to the legacy path below.
}
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "true");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
textarea.style.pointerEvents = "none";
document.body.appendChild(textarea);
textarea.select();
let copied = false;
try {
copied = document.execCommand("copy");
} catch {
copied = false;
} finally {
document.body.removeChild(textarea);
}
return copied;
}
function GroupChevronIcon({ expanded }: { expanded: boolean }) {
return (
<svg className={expanded ? "autoruns-group-chevron-svg expanded" : "autoruns-group-chevron-svg"} viewBox="0 0 16 16" aria-hidden="true" focusable="false">
@@ -697,6 +729,9 @@ export function AutoRunsHistoryPanel({
const [dialogBusy, setDialogBusy] = useState(false);
const [annotationsBusy, setAnnotationsBusy] = useState(false);
const [annotationResolutionBusyId, setAnnotationResolutionBusyId] = useState("");
const dialogCopyResetTimerRef = useRef<number | null>(null);
const [dialogCopyState, setDialogCopyState] = useState<"idle" | "success" | "error">("idle");
const [dialogCopyModeLabel, setDialogCopyModeLabel] = useState<"чат" | "тех">("чат");
const [errorText, setErrorText] = useState("");
const [assistantLiveSessionId, setAssistantLiveSessionId] = useState("");
const [assistantLiveConversation, setAssistantLiveConversation] = useState<AssistantConversationItem[]>([]);
@@ -941,6 +976,14 @@ export function AutoRunsHistoryPanel({
});
}, []);
useEffect(() => {
return () => {
if (dialogCopyResetTimerRef.current !== null) {
window.clearTimeout(dialogCopyResetTimerRef.current);
}
};
}, []);
const copyIdentifierToClipboard = useCallback(
async (event: React.SyntheticEvent, valueRaw: string, label: string) => {
event.stopPropagation();
@@ -950,19 +993,7 @@ export function AutoRunsHistoryPanel({
return;
}
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
} else {
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.setAttribute("readonly", "true");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}
await writeTextToClipboard(value);
log(`${label} copied: ${value}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
@@ -973,6 +1004,49 @@ export function AutoRunsHistoryPanel({
[log]
);
const copyDialogConversation = useCallback(
async (mode: ConversationExportMode) => {
if (!dialog || dialog.messages.length === 0 || !selectedRunId) {
return;
}
const exportText = buildAutoRunDialogExportForCopy(
{
runId: selectedRunId,
caseId: selectedCaseId || dialog.case_id || "n/a",
sessionId: dialog.session_id,
source: dialog.source,
messages: dialog.messages,
decomposition: dialog.decomposition,
assistantMode: dialog.assistant_mode,
annotations: dialog.annotations,
runSummary: runDetail?.run ?? null,
coverage: runDetail?.coverage ?? null,
report: runDetail?.report ?? null
},
mode
);
const copied = await writeTextToClipboard(exportText);
setDialogCopyModeLabel(mode === "technical" ? "тех" : "чат");
setDialogCopyState(copied ? "success" : "error");
if (dialogCopyResetTimerRef.current !== null) {
window.clearTimeout(dialogCopyResetTimerRef.current);
}
dialogCopyResetTimerRef.current = window.setTimeout(() => {
setDialogCopyState("idle");
}, 2200);
if (copied) {
log(`Dialog ${mode === "technical" ? "technical" : "chat"} copied: run=${selectedRunId} case=${selectedCaseId || dialog.case_id}`);
} else {
log(`Dialog copy failed: run=${selectedRunId} case=${selectedCaseId || dialog.case_id}`);
}
},
[dialog, log, runDetail, selectedCaseId, selectedRunId]
);
function startAssistantLiveStatusTicker(): () => void {
let index = 0;
setAssistantLiveStatus(ASSISTANT_STAGES[0]);
@@ -3419,6 +3493,34 @@ export function AutoRunsHistoryPanel({
))}
</select>
</label>
<div className="autoruns-dialog-copy-actions">
<button
type="button"
className="assistant-copy-btn"
onClick={() => {
void copyDialogConversation("default");
}}
disabled={dialogBusy || detailBusy || (dialog?.messages.length ?? 0) === 0}
title="Скопировать question-answer диалог текущего прогона"
>
Скопировать чат
</button>
<button
type="button"
className="assistant-copy-btn"
onClick={() => {
void copyDialogConversation("technical");
}}
disabled={dialogBusy || detailBusy || (dialog?.messages.length ?? 0) === 0}
title="Скопировать диалог вместе с debug JSON и метаданными прогона"
>
Скопировать техчат
</button>
<div className="autoruns-dialog-copy-status">
{dialogCopyState === "success" ? <span className="assistant-copy-feedback success">Скопировано ({dialogCopyModeLabel})</span> : null}
{dialogCopyState === "error" ? <span className="assistant-copy-feedback error">Ошибка копирования</span> : null}
</div>
</div>
</div>
</div>
@@ -248,6 +248,7 @@ export interface AutoRunDialogMessage {
created_at: string | null;
trace_id: string | null;
reply_type: string | null;
debug?: unknown | null;
message_index: number;
case_id?: string | null;
case_message_index?: number | null;
+15
View File
@@ -1263,6 +1263,21 @@ button:disabled {
gap: 8px;
}
.autoruns-dialog-copy-actions {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
align-items: center;
}
.autoruns-dialog-copy-status {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
min-height: 18px;
}
.autoruns-case-list {
margin-top: 8px;
display: grid;
@@ -10,6 +10,33 @@ export interface ConversationExportItem {
debug?: unknown | null;
}
export interface AutoRunDialogExportItem {
message_id: string | null;
role: string;
text: string;
reply_type: string | null;
created_at: string | null;
trace_id: string | null;
message_index: number;
case_id?: string | null;
case_message_index?: number | null;
debug?: unknown | null;
}
export interface AutoRunDialogExportPayload {
runId: string;
caseId: string;
sessionId: string;
source: string;
messages: AutoRunDialogExportItem[];
decomposition?: string[];
assistantMode?: unknown | null;
annotations?: unknown[];
runSummary?: unknown | null;
coverage?: unknown | null;
report?: unknown | null;
}
const DEBUG_SECTION_PATTERN =
/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json|debug_payload|technical_breakdown)\b/i;
@@ -28,6 +55,10 @@ function stringifyDebug(value: unknown): string {
}
}
function normalizeRole(value: string): "user" | "assistant" {
return value === "assistant" ? "assistant" : "user";
}
export function sanitizeConversationExportText(value: string): string {
const raw = String(value ?? "");
const cutMatch = raw.match(DEBUG_SECTION_PATTERN);
@@ -83,3 +114,113 @@ export function buildConversationExportForCopy(
return lines.join("\n");
}
export function buildAutoRunDialogExportForCopy(
payload: AutoRunDialogExportPayload,
mode: ConversationExportMode = "default"
): string {
const includeDebug = mode === "technical";
const lines: string[] = [];
lines.push("# Autorun dialog export");
lines.push(`run_id: ${payload.runId || "n/a"}`);
lines.push(`case_id: ${payload.caseId || "n/a"}`);
lines.push(`session_id: ${payload.sessionId || "n/a"}`);
lines.push(`source: ${payload.source || "n/a"}`);
lines.push(`export_mode: ${mode}`);
lines.push(`exported_at: ${new Date().toISOString()}`);
lines.push("");
for (let index = 0; index < payload.messages.length; index += 1) {
const item = payload.messages[index];
const role = normalizeRole(item.role);
const safeText = sanitizeConversationExportText(item.text || "");
lines.push(`## ${index + 1}. ${role}`);
lines.push(`message_index: ${item.message_index}`);
if (item.case_id) {
lines.push(`case_id: ${item.case_id}`);
}
if (typeof item.case_message_index === "number") {
lines.push(`case_message_index: ${item.case_message_index}`);
}
if (item.created_at) {
lines.push(`created_at: ${item.created_at}`);
}
if (includeDebug) {
lines.push(`reply_type: ${item.reply_type ?? "n/a"}`);
if (item.trace_id) {
lines.push(`trace_id: ${item.trace_id}`);
}
}
lines.push("");
lines.push(safeText || "(empty)");
lines.push("");
if (includeDebug && role === "assistant" && item.debug) {
lines.push("### technical_debug_payload_json");
lines.push("```json");
lines.push(stringifyDebug(item.debug));
lines.push("```");
lines.push("");
}
}
if (!includeDebug) {
return lines.join("\n");
}
lines.push("### dialog_messages_json");
lines.push("```json");
lines.push(stringifyDebug(payload.messages));
lines.push("```");
lines.push("");
if ((payload.decomposition ?? []).length > 0) {
lines.push("### decomposition_json");
lines.push("```json");
lines.push(stringifyDebug(payload.decomposition));
lines.push("```");
lines.push("");
}
if (payload.assistantMode) {
lines.push("### assistant_mode_json");
lines.push("```json");
lines.push(stringifyDebug(payload.assistantMode));
lines.push("```");
lines.push("");
}
if ((payload.annotations ?? []).length > 0) {
lines.push("### annotations_json");
lines.push("```json");
lines.push(stringifyDebug(payload.annotations));
lines.push("```");
lines.push("");
}
if (payload.runSummary) {
lines.push("### run_summary_json");
lines.push("```json");
lines.push(stringifyDebug(payload.runSummary));
lines.push("```");
lines.push("");
}
if (payload.coverage) {
lines.push("### coverage_json");
lines.push("```json");
lines.push(stringifyDebug(payload.coverage));
lines.push("```");
lines.push("");
}
if (payload.report) {
lines.push("### run_report_json");
lines.push("```json");
lines.push(stringifyDebug(payload.report));
lines.push("```");
lines.push("");
}
return lines.join("\n");
}