ДОМЕНЫ - ВОПРОСЫ - fix(ui-assistant-chat): render markdown bold, add readable block spacing, force scroll-to-bottom on send + ЮИ
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-CVIU9teH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D4dtBq8A.css">
|
||||
<script type="module" crossorigin src="/assets/index-CEJTyo9A.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cx2J_KsF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -74,6 +74,95 @@ function CommentBubbleIcon({ commented }: { commented: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeAssistantMessageText(raw: string): string {
|
||||
return raw
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.replace(/([^\n])\s+(Блок\s+\d+\.)/gi, "$1\n\n$2")
|
||||
.replace(/([^\n])\s+(\d+\.\s)/g, "$1\n$2");
|
||||
}
|
||||
|
||||
function splitAssistantMessageBlocks(text: string): string[] {
|
||||
const normalized = normalizeAssistantMessageText(text);
|
||||
const lines = normalized.split("\n");
|
||||
const blocks: string[] = [];
|
||||
let current: string[] = [];
|
||||
|
||||
const flush = () => {
|
||||
if (current.length === 0) return;
|
||||
blocks.push(current.join("\n"));
|
||||
current = [];
|
||||
};
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trimEnd();
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
|
||||
const isBlockHeading = /^Блок\s+\d+\./i.test(trimmed);
|
||||
const isNumberedItem = /^\d+\.\s/.test(trimmed);
|
||||
if ((isBlockHeading || isNumberedItem) && current.length > 0) {
|
||||
flush();
|
||||
}
|
||||
|
||||
current.push(line);
|
||||
}
|
||||
|
||||
flush();
|
||||
return blocks.length > 0 ? blocks : [text];
|
||||
}
|
||||
|
||||
function renderInlineBold(text: string, keyPrefix: string): JSX.Element[] {
|
||||
const result: JSX.Element[] = [];
|
||||
const regex = /\*\*(.+?)\*\*/g;
|
||||
let lastIndex = 0;
|
||||
let partIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
result.push(<span key={`${keyPrefix}-t-${partIndex}`}>{text.slice(lastIndex, match.index)}</span>);
|
||||
partIndex += 1;
|
||||
}
|
||||
|
||||
result.push(<strong key={`${keyPrefix}-b-${partIndex}`}>{match[1]}</strong>);
|
||||
partIndex += 1;
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
result.push(<span key={`${keyPrefix}-t-${partIndex}`}>{text.slice(lastIndex)}</span>);
|
||||
}
|
||||
|
||||
return result.length > 0 ? result : [<span key={`${keyPrefix}-raw`}>{text}</span>];
|
||||
}
|
||||
|
||||
function lineClassName(line: string): string {
|
||||
const trimmed = line.trimStart();
|
||||
if (/^Блок\s+\d+\./i.test(trimmed)) return "assistant-msg-line heading";
|
||||
if (/^\d+\.\s/.test(trimmed)) return "assistant-msg-line numbered";
|
||||
if (/^-\s/.test(trimmed)) return "assistant-msg-line bullet";
|
||||
return "assistant-msg-line";
|
||||
}
|
||||
|
||||
function renderAssistantMessageBody(text: string): JSX.Element[] {
|
||||
const blocks = splitAssistantMessageBlocks(text);
|
||||
return blocks.map((block, blockIndex) => {
|
||||
const lines = block.split("\n");
|
||||
return (
|
||||
<div key={`block-${blockIndex}`} className="assistant-msg-block">
|
||||
{lines.map((line, lineIndex) => (
|
||||
<p key={`line-${blockIndex}-${lineIndex}`} className={lineClassName(line)}>
|
||||
{renderInlineBold(line, `line-${blockIndex}-${lineIndex}`)}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function AssistantPanel({
|
||||
sessionId,
|
||||
conversation,
|
||||
@@ -97,9 +186,17 @@ export function AssistantPanel({
|
||||
const [copyState, setCopyState] = useState<"idle" | "success" | "error">("idle");
|
||||
const [copyModeLabel, setCopyModeLabel] = useState<"чат" | "тех">("чат");
|
||||
|
||||
function scrollChatToBottom(forceStick = false): void {
|
||||
if (!listRef.current) return;
|
||||
if (forceStick) {
|
||||
stickToBottomRef.current = true;
|
||||
}
|
||||
listRef.current.scrollTop = listRef.current.scrollHeight;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (listRef.current && stickToBottomRef.current) {
|
||||
listRef.current.scrollTop = listRef.current.scrollHeight;
|
||||
if (stickToBottomRef.current) {
|
||||
scrollChatToBottom();
|
||||
}
|
||||
}, [conversation]);
|
||||
|
||||
@@ -219,7 +316,7 @@ export function AssistantPanel({
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="assistant-msg-body">{item.text}</div>
|
||||
<div className="assistant-msg-body">{renderAssistantMessageBody(item.text)}</div>
|
||||
{item.role === "assistant" && item.debug ? (
|
||||
<details className="assistant-debug">
|
||||
<summary>Показать технический разбор</summary>
|
||||
@@ -247,7 +344,15 @@ export function AssistantPanel({
|
||||
<input type="checkbox" checked={useMock} onChange={(event) => onUseMockChange(event.target.checked)} />
|
||||
Mock-режим
|
||||
</label>
|
||||
<button type="button" className="assistant-send-btn" onClick={() => onSend()} disabled={busy || !inputValue.trim()}>
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-send-btn"
|
||||
onClick={() => {
|
||||
scrollChatToBottom(true);
|
||||
void onSend();
|
||||
}}
|
||||
disabled={busy || !inputValue.trim()}
|
||||
>
|
||||
{busy ? "Выполняю..." : "Отправить"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -548,11 +548,36 @@ button:disabled {
|
||||
}
|
||||
|
||||
.assistant-msg-body {
|
||||
white-space: pre-wrap;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
line-height: 1.35;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.assistant-msg-block {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.assistant-msg-line {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.assistant-msg-line.heading {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.assistant-msg-line.numbered {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.assistant-msg-line strong {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.assistant-trace {
|
||||
margin-top: 6px;
|
||||
color: var(--text-muted);
|
||||
|
||||
Reference in New Issue
Block a user