Усилить answer contract и агентный аудит для phase105
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user