Stage 2 завершён: problem-first ответы и follow-up continuity - ассистент переведён от entity-heavy логики к problem-first ответам с problem-unit слоем, удержанием контекста в follow-up и очисткой пользовательского ответа от сырых технических ссылок.
This commit is contained in:
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
File diff suppressed because one or more lines are too long
+2
-2
@@ -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-B-bUXClb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D1qe5c2Q.css">
|
||||
<script type="module" crossorigin src="/assets/index-OzA7Q0i7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Ch7jCAii.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AssistantConversationItem } from "../state/types";
|
||||
import { JsonView } from "./JsonView";
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
@@ -33,6 +33,77 @@ 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[]): 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 (item.role === "assistant" && item.debug) {
|
||||
lines.push("### technical_breakdown_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 {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch {
|
||||
// Fall back to 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;
|
||||
}
|
||||
|
||||
export function AssistantPanel({
|
||||
sessionId,
|
||||
conversation,
|
||||
@@ -51,6 +122,8 @@ export function AssistantPanel({
|
||||
errorMessage
|
||||
}: AssistantPanelProps) {
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const copyResetTimerRef = useRef<number | null>(null);
|
||||
const [copyState, setCopyState] = useState<"idle" | "success" | "error">("idle");
|
||||
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
@@ -58,11 +131,51 @@ export function AssistantPanel({
|
||||
}
|
||||
}, [conversation, statusText]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (copyResetTimerRef.current !== null) {
|
||||
window.clearTimeout(copyResetTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function handleCopyConversation(): Promise<void> {
|
||||
if (conversation.length === 0) {
|
||||
return;
|
||||
}
|
||||
const exportText = buildConversationExport(sessionId, conversation);
|
||||
const copied = await copyTextToClipboard(exportText);
|
||||
setCopyState(copied ? "success" : "error");
|
||||
|
||||
if (copyResetTimerRef.current !== null) {
|
||||
window.clearTimeout(copyResetTimerRef.current);
|
||||
}
|
||||
copyResetTimerRef.current = window.setTimeout(() => {
|
||||
setCopyState("idle");
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelFrame
|
||||
title="Режим ассистента"
|
||||
subtitle="Диалоговый слой поверх normalizer, маршрутизации и factual retrieval."
|
||||
actions={<span className="status-chip">{sessionId ? `session: ${sessionId}` : "новая сессия"}</span>}
|
||||
actions={
|
||||
<div className="assistant-panel-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-copy-btn"
|
||||
onClick={() => {
|
||||
void handleCopyConversation();
|
||||
}}
|
||||
disabled={conversation.length === 0}
|
||||
>
|
||||
Скопировать чат
|
||||
</button>
|
||||
{copyState === "success" ? <span className="assistant-copy-feedback success">Скопировано</span> : null}
|
||||
{copyState === "error" ? <span className="assistant-copy-feedback error">Ошибка копирования</span> : null}
|
||||
<span className="status-chip">{sessionId ? `session: ${sessionId}` : "новая сессия"}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div ref={listRef} className="assistant-chat-list">
|
||||
{conversation.length === 0 ? (
|
||||
|
||||
@@ -117,6 +117,42 @@ body,
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.assistant-panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-copy-btn {
|
||||
background: transparent;
|
||||
border-color: var(--line);
|
||||
color: var(--text-main);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.assistant-copy-btn:hover {
|
||||
background: rgba(143, 255, 173, 0.14);
|
||||
filter: none;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.assistant-copy-feedback {
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.assistant-copy-feedback.success {
|
||||
color: var(--lime-main);
|
||||
}
|
||||
|
||||
.assistant-copy-feedback.error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
|
||||
Reference in New Issue
Block a user