Усилить 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
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-6meEannb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BURU4_Sm.css">
<script type="module" crossorigin src="/assets/index-9w9i5XPJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DW_SonhM.css">
</head>
<body>
<div id="root"></div>
@@ -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");
}