From cb115f1a88fc39ddf7550690609efda89e1bb815 Mon Sep 17 00:00:00 2001 From: sp mac bookpro 2605 Date: Fri, 29 May 2026 20:43:28 +0800 Subject: [PATCH] fix(chat): respect user scroll and tighten markdown --- src/api.ts | 7 ++++++- src/pages/ChatPage.tsx | 22 ++++++++++++++++++---- src/styles.css | 24 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/api.ts b/src/api.ts index 10ec045..20a8db5 100644 --- a/src/api.ts +++ b/src/api.ts @@ -612,6 +612,7 @@ async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) const { value, done } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); + buf = buf.replace(/\r\n/g, '\n'); let idx; while ((idx = buf.indexOf('\n\n')) !== -1) { @@ -623,7 +624,11 @@ async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) let dataStr = ''; for (const line of raw.split('\n')) { if (line.startsWith('event:')) event = line.slice(6).trim(); - else if (line.startsWith('data:')) dataStr += line.slice(5).trim(); + else if (line.startsWith('data:')) { + let part = line.slice(5); + if (part.startsWith(' ')) part = part.slice(1); + dataStr += (dataStr ? '\n' : '') + part; + } } if (!dataStr) continue; let data: any; diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 8fea917..7f1f296 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -72,6 +72,7 @@ export default function ChatPage() { const bodyRef = useRef(null); const abortRef = useRef(null); const creatingSessionRef = useRef(false); + const autoScrollRef = useRef(true); // URL 参数 ?session=xxx&msg=yyy useEffect(() => { @@ -88,12 +89,25 @@ export default function ChatPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]); - const scrollBottom = () => { + const scrollBottom = (force = false) => { + if (!force && !autoScrollRef.current) return; requestAnimationFrame(() => { bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight, behavior: 'smooth' }); }); }; + useEffect(() => { + const el = bodyRef.current; + if (!el) return; + const onScroll = () => { + const distance = el.scrollHeight - el.scrollTop - el.clientHeight; + autoScrollRef.current = distance < 32; + }; + el.addEventListener('scroll', onScroll, { passive: true }); + onScroll(); + return () => el.removeEventListener('scroll', onScroll); + }, []); + const loadAgent = async () => { if (!id) { setAgent(null); @@ -198,7 +212,7 @@ export default function ChatPage() { retrieved: [], toolCalls: [] }); - scrollBottom(); + scrollBottom(true); abortRef.current?.abort(); const ctrl = new AbortController(); @@ -265,7 +279,7 @@ export default function ChatPage() { setSessionRefresh((t) => t + 1); setAttachments([]); // 用完即清 setImageUrls([]); - scrollBottom(); + scrollBottom(true); }, onAborted: (data) => { // 已停止:保留已生成内容;user 消息也留下(用 tempUser 占位) @@ -322,7 +336,7 @@ export default function ChatPage() { createdAt: Date.now() }; setMessages((m) => [...(m || []), tempUser]); - scrollBottom(); + scrollBottom(true); const attText = buildAttachmentsText(); const content = attText ? `${text}\n\n${attText}` : text; const model = overrides.model || parseAgentModels(agent?.model)[0] || ''; diff --git a/src/styles.css b/src/styles.css index abcd3ca..c32861d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -539,6 +539,30 @@ body { margin-bottom: 0; } +.bubble.assistant h1, +.bubble.assistant h2, +.bubble.assistant h3, +.bubble.assistant h4, +.bubble.assistant h5, +.bubble.assistant h6 { + margin: 0.5em 0 0.25em; + line-height: 1.3; +} + +.bubble.assistant ol, +.bubble.assistant ul { + margin: 0.4em 0; + padding-left: 1.25em; +} + +.bubble.assistant li { + margin: 0.15em 0; +} + +.bubble.assistant hr { + margin: 0.8em 0; +} + .chat-input-wrapper { width: 100%; max-width: 820px;