Этап 4 / Волна 18 закрытие блокеров по времени, доменной полярности и допуску доказательной базы
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AssistantConversationItem } from "../state/types";
|
||||
import { buildConversationExportForCopy } from "../utils/conversationExport";
|
||||
import { JsonView } from "./JsonView";
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
|
||||
@@ -33,50 +34,6 @@ function shortTime(iso: string): string {
|
||||
return date.toLocaleTimeString("ru-RU");
|
||||
}
|
||||
|
||||
function stringifyDebug(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function buildConversationExport(
|
||||
sessionId: string,
|
||||
conversation: AssistantConversationItem[],
|
||||
includeDebugPayload = false
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("# Assistant conversation export");
|
||||
lines.push(`session_id: ${sessionId || "n/a"}`);
|
||||
lines.push(`exported_at: ${new Date().toISOString()}`);
|
||||
lines.push("");
|
||||
|
||||
for (let index = 0; index < conversation.length; index += 1) {
|
||||
const item = conversation[index];
|
||||
lines.push(`## ${index + 1}. ${item.role}`);
|
||||
lines.push(`message_id: ${item.message_id}`);
|
||||
lines.push(`created_at: ${item.created_at}`);
|
||||
lines.push(`reply_type: ${item.reply_type ?? "n/a"}`);
|
||||
if (item.trace_id) {
|
||||
lines.push(`trace_id: ${item.trace_id}`);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(item.text || "(empty)");
|
||||
lines.push("");
|
||||
|
||||
if (includeDebugPayload && item.role === "assistant" && item.debug) {
|
||||
lines.push("### debug_payload_json");
|
||||
lines.push("```json");
|
||||
lines.push(stringifyDebug(item.debug));
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text: string): Promise<boolean> {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
try {
|
||||
@@ -128,6 +85,7 @@ export function AssistantPanel({
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const copyResetTimerRef = useRef<number | null>(null);
|
||||
const [copyState, setCopyState] = useState<"idle" | "success" | "error">("idle");
|
||||
const [copyModeLabel, setCopyModeLabel] = useState<"чат" | "тех">("чат");
|
||||
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
@@ -143,13 +101,14 @@ export function AssistantPanel({
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function handleCopyConversation(): Promise<void> {
|
||||
async function handleCopyConversation(mode: "default" | "technical"): Promise<void> {
|
||||
if (conversation.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Copy full run context for diagnostics (including debug payload blocks).
|
||||
const exportText = buildConversationExport(sessionId, conversation, true);
|
||||
|
||||
const exportText = buildConversationExportForCopy(sessionId, conversation, mode);
|
||||
const copied = await copyTextToClipboard(exportText);
|
||||
setCopyModeLabel(mode === "technical" ? "тех" : "чат");
|
||||
setCopyState(copied ? "success" : "error");
|
||||
|
||||
if (copyResetTimerRef.current !== null) {
|
||||
@@ -170,13 +129,25 @@ export function AssistantPanel({
|
||||
type="button"
|
||||
className="assistant-copy-btn"
|
||||
onClick={() => {
|
||||
void handleCopyConversation();
|
||||
void handleCopyConversation("default");
|
||||
}}
|
||||
disabled={conversation.length === 0}
|
||||
title="Экспорт только user-facing чата"
|
||||
>
|
||||
Скопировать чат
|
||||
</button>
|
||||
{copyState === "success" ? <span className="assistant-copy-feedback success">Скопировано</span> : null}
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-copy-btn"
|
||||
onClick={() => {
|
||||
void handleCopyConversation("technical");
|
||||
}}
|
||||
disabled={conversation.length === 0}
|
||||
title="Технический экспорт с debug payload"
|
||||
>
|
||||
Скопировать техчат
|
||||
</button>
|
||||
{copyState === "success" ? <span className="assistant-copy-feedback success">Скопировано ({copyModeLabel})</span> : null}
|
||||
{copyState === "error" ? <span className="assistant-copy-feedback error">Ошибка копирования</span> : null}
|
||||
<span className="status-chip">{sessionId ? `session: ${sessionId}` : "новая сессия"}</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
export type ConversationExportMode = "default" | "technical";
|
||||
|
||||
export interface ConversationExportItem {
|
||||
message_id: string;
|
||||
role: "user" | "assistant";
|
||||
text: string;
|
||||
reply_type: string | null;
|
||||
created_at: string;
|
||||
trace_id: string | null;
|
||||
debug?: 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;
|
||||
|
||||
const INLINE_TECH_LINE_PATTERNS: RegExp[] = [
|
||||
/\b(?:debug_payload_json|technical_breakdown_json)\b/i,
|
||||
/\b(?:route_summary|semantic_profile|domain_scope|relation_patterns|account_scope)\b/i,
|
||||
/\b(?:coverage_report|retrieval_status|problem_unit_state|candidate_evidence)\b/i,
|
||||
/\b(?:graph_domain_scope|graph_runtime|selection_reason|why_included)\b/i
|
||||
];
|
||||
|
||||
function stringifyDebug(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeConversationExportText(value: string): string {
|
||||
const raw = String(value ?? "");
|
||||
const cutMatch = raw.match(DEBUG_SECTION_PATTERN);
|
||||
const preCut = cutMatch ? raw.slice(0, cutMatch.index) : raw;
|
||||
const withoutDebugHeadings = preCut
|
||||
.replace(/###\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json)[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
|
||||
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json)\b[\s\S]*$/gi, "");
|
||||
|
||||
const lines = withoutDebugHeadings
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => line.trimEnd())
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.filter((line) => !INLINE_TECH_LINE_PATTERNS.some((pattern) => pattern.test(line)));
|
||||
|
||||
return lines.join("\n").trim();
|
||||
}
|
||||
|
||||
export function buildConversationExportForCopy(
|
||||
sessionId: string,
|
||||
conversation: ConversationExportItem[],
|
||||
mode: ConversationExportMode = "default"
|
||||
): string {
|
||||
const includeDebug = mode === "technical";
|
||||
const lines: string[] = [];
|
||||
lines.push("# Assistant conversation export");
|
||||
lines.push(`session_id: ${sessionId || "n/a"}`);
|
||||
lines.push(`export_mode: ${mode}`);
|
||||
lines.push(`exported_at: ${new Date().toISOString()}`);
|
||||
lines.push("");
|
||||
|
||||
for (let index = 0; index < conversation.length; index += 1) {
|
||||
const item = conversation[index];
|
||||
const safeText = sanitizeConversationExportText(item.text || "");
|
||||
lines.push(`## ${index + 1}. ${item.role}`);
|
||||
lines.push(`message_id: ${item.message_id}`);
|
||||
lines.push(`created_at: ${item.created_at}`);
|
||||
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 && item.role === "assistant" && item.debug) {
|
||||
lines.push("### technical_debug_payload_json");
|
||||
lines.push("```json");
|
||||
lines.push(stringifyDebug(item.debug));
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
Reference in New Issue
Block a user