:lightning: optimize chat streaming
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, memo } from 'react'
|
||||
import { marked } from 'marked'
|
||||
import DOMPurify from 'dompurify'
|
||||
import type { ChatMessage, TextAnnotation } from '../../types/editor'
|
||||
@@ -30,7 +30,7 @@ interface Props {
|
||||
linkedAnnotations?: TextAnnotation[]
|
||||
}
|
||||
|
||||
export function ChatMessageItem({ message, linkedAnnotations }: Props): JSX.Element {
|
||||
function ChatMessageItemInner({ message, linkedAnnotations }: Props): JSX.Element {
|
||||
const { activeFilePath, markSaved } = useEditorStore()
|
||||
|
||||
const html = useMemo(() => {
|
||||
@@ -123,3 +123,13 @@ export function ChatMessageItem({ message, linkedAnnotations }: Props): JSX.Elem
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function areEqual(prev: Props, next: Props): boolean {
|
||||
if (prev.message.content !== next.message.content) return false
|
||||
if (prev.message.id !== next.message.id) return false
|
||||
const prevIds = prev.linkedAnnotations?.map(a => a.id).join(',') ?? ''
|
||||
const nextIds = next.linkedAnnotations?.map(a => a.id).join(',') ?? ''
|
||||
return prevIds === nextIds
|
||||
}
|
||||
|
||||
export const ChatMessageItem = memo(ChatMessageItemInner, areEqual)
|
||||
|
||||
@@ -50,6 +50,7 @@ export function ChatPanel(): JSX.Element {
|
||||
const [pendingAttachmentCount, setPendingAttachmentCount] = useState(0)
|
||||
const [feedbackNotice, setFeedbackNotice] = useState<string | null>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const scrollRafRef = useRef<number | null>(null)
|
||||
const prevAnnotationCountRef = useRef(annotations.length)
|
||||
|
||||
// Collapse history when switching files
|
||||
@@ -119,12 +120,14 @@ export function ChatPanel(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom when new content arrives
|
||||
// Auto-scroll to bottom when new content arrives (coalesced to one scroll per animation frame)
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight
|
||||
}
|
||||
if (scrollRafRef.current !== null) return
|
||||
scrollRafRef.current = requestAnimationFrame(() => {
|
||||
scrollRafRef.current = null
|
||||
const el = scrollRef.current
|
||||
if (el) el.scrollTop = el.scrollHeight
|
||||
})
|
||||
}, [chatHistory])
|
||||
|
||||
const hasFile = Boolean(activeFilePath)
|
||||
|
||||
@@ -256,17 +256,24 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
||||
if (last?.role === 'assistant') {
|
||||
history[history.length - 1] = { ...last, content: last.content + chunk }
|
||||
}
|
||||
if (!s.activeFilePath) return { chatHistory: history }
|
||||
const activeId = s.activeSessionIdByFile[s.activeFilePath]
|
||||
const sessions = (s.chatSessionsByFile[s.activeFilePath] ?? []).map(sess =>
|
||||
sess.id === activeId ? { ...sess, messages: history } : sess
|
||||
)
|
||||
return { chatHistory: history, chatSessionsByFile: { ...s.chatSessionsByFile, [s.activeFilePath]: sessions } }
|
||||
return { chatHistory: history }
|
||||
})
|
||||
},
|
||||
|
||||
setAILoading: (isAILoading) => {
|
||||
set({ isAILoading })
|
||||
set((s) => {
|
||||
if (isAILoading) return { isAILoading }
|
||||
// Sync the completed chatHistory into chatSessionsByFile once at stream end
|
||||
if (!s.activeFilePath) return { isAILoading }
|
||||
const activeId = s.activeSessionIdByFile[s.activeFilePath]
|
||||
const sessions = (s.chatSessionsByFile[s.activeFilePath] ?? []).map(sess =>
|
||||
sess.id === activeId ? { ...sess, messages: s.chatHistory } : sess
|
||||
)
|
||||
return {
|
||||
isAILoading,
|
||||
chatSessionsByFile: { ...s.chatSessionsByFile, [s.activeFilePath]: sessions }
|
||||
}
|
||||
})
|
||||
// When AI finishes, persist the completed chat history
|
||||
if (!isAILoading) {
|
||||
scheduleSave(() => {
|
||||
|
||||
Reference in New Issue
Block a user