Stage 2 завершён: problem-first ответы и follow-up continuity - ассистент переведён от entity-heavy логики к problem-first ответам с problem-unit слоем, удержанием контекста в follow-up и очисткой пользовательского ответа от сырых технических ссылок.

This commit is contained in:
2026-03-26 14:53:52 +03:00
parent ece1abed76
commit 96353cfd48
2474 changed files with 21678 additions and 3292445 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
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-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 ? (
+36
View File
@@ -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,