fix(chat): respect user scroll and tighten markdown
parent
f7abd99a3e
commit
cb115f1a88
|
|
@ -612,6 +612,7 @@ async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal)
|
||||||
const { value, done } = await reader.read();
|
const { value, done } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
buf += decoder.decode(value, { stream: true });
|
buf += decoder.decode(value, { stream: true });
|
||||||
|
buf = buf.replace(/\r\n/g, '\n');
|
||||||
|
|
||||||
let idx;
|
let idx;
|
||||||
while ((idx = buf.indexOf('\n\n')) !== -1) {
|
while ((idx = buf.indexOf('\n\n')) !== -1) {
|
||||||
|
|
@ -623,7 +624,11 @@ async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal)
|
||||||
let dataStr = '';
|
let dataStr = '';
|
||||||
for (const line of raw.split('\n')) {
|
for (const line of raw.split('\n')) {
|
||||||
if (line.startsWith('event:')) event = line.slice(6).trim();
|
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;
|
if (!dataStr) continue;
|
||||||
let data: any;
|
let data: any;
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ export default function ChatPage() {
|
||||||
const bodyRef = useRef<HTMLDivElement>(null);
|
const bodyRef = useRef<HTMLDivElement>(null);
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
const creatingSessionRef = useRef(false);
|
const creatingSessionRef = useRef(false);
|
||||||
|
const autoScrollRef = useRef(true);
|
||||||
|
|
||||||
// URL 参数 ?session=xxx&msg=yyy
|
// URL 参数 ?session=xxx&msg=yyy
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -88,12 +89,25 @@ export default function ChatPage() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
const scrollBottom = () => {
|
const scrollBottom = (force = false) => {
|
||||||
|
if (!force && !autoScrollRef.current) return;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight, behavior: 'smooth' });
|
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 () => {
|
const loadAgent = async () => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
setAgent(null);
|
setAgent(null);
|
||||||
|
|
@ -198,7 +212,7 @@ export default function ChatPage() {
|
||||||
retrieved: [],
|
retrieved: [],
|
||||||
toolCalls: []
|
toolCalls: []
|
||||||
});
|
});
|
||||||
scrollBottom();
|
scrollBottom(true);
|
||||||
|
|
||||||
abortRef.current?.abort();
|
abortRef.current?.abort();
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
|
|
@ -265,7 +279,7 @@ export default function ChatPage() {
|
||||||
setSessionRefresh((t) => t + 1);
|
setSessionRefresh((t) => t + 1);
|
||||||
setAttachments([]); // 用完即清
|
setAttachments([]); // 用完即清
|
||||||
setImageUrls([]);
|
setImageUrls([]);
|
||||||
scrollBottom();
|
scrollBottom(true);
|
||||||
},
|
},
|
||||||
onAborted: (data) => {
|
onAborted: (data) => {
|
||||||
// 已停止:保留已生成内容;user 消息也留下(用 tempUser 占位)
|
// 已停止:保留已生成内容;user 消息也留下(用 tempUser 占位)
|
||||||
|
|
@ -322,7 +336,7 @@ export default function ChatPage() {
|
||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
};
|
};
|
||||||
setMessages((m) => [...(m || []), tempUser]);
|
setMessages((m) => [...(m || []), tempUser]);
|
||||||
scrollBottom();
|
scrollBottom(true);
|
||||||
const attText = buildAttachmentsText();
|
const attText = buildAttachmentsText();
|
||||||
const content = attText ? `${text}\n\n${attText}` : text;
|
const content = attText ? `${text}\n\n${attText}` : text;
|
||||||
const model = overrides.model || parseAgentModels(agent?.model)[0] || '';
|
const model = overrides.model || parseAgentModels(agent?.model)[0] || '';
|
||||||
|
|
|
||||||
|
|
@ -539,6 +539,30 @@ body {
|
||||||
margin-bottom: 0;
|
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 {
|
.chat-input-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 820px;
|
max-width: 820px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue