fix: avoid rerendering chat history while typing
This commit is contained in:
@@ -44,6 +44,19 @@ const speechRecognitionMocks = vi.hoisted(() => ({
|
||||
startPcmRecording: vi.fn()
|
||||
}))
|
||||
|
||||
const messageRenderProbe = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('./MarkdownRenderer', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('./MarkdownRenderer')>()
|
||||
return {
|
||||
...original,
|
||||
MarkdownRenderer: (props: { children: string; renderHtml?: boolean }) => {
|
||||
messageRenderProbe(props.children)
|
||||
return <original.MarkdownRenderer {...props} />
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const lazyRouteMocks = vi.hoisted(() => {
|
||||
let pending: Promise<void> | undefined
|
||||
let releasePending: (() => void) | undefined
|
||||
@@ -2527,6 +2540,28 @@ describe('App', () => {
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('measures composer height once per input and resizes after sending', async () => {
|
||||
render(<App />)
|
||||
const input = await screen.findByLabelText('向 GoodBuddy 提问')
|
||||
const measureHeight = vi.fn(() =>
|
||||
(input as HTMLTextAreaElement).value ? 300 : 0
|
||||
)
|
||||
Object.defineProperty(input, 'scrollHeight', {
|
||||
configurable: true,
|
||||
get: measureHeight
|
||||
})
|
||||
|
||||
fireEvent.input(input, { target: { value: 'First line\nSecond line' } })
|
||||
expect(measureHeight).toHaveBeenCalledTimes(1)
|
||||
expect(input).toHaveStyle({ height: '220px' })
|
||||
|
||||
measureHeight.mockClear()
|
||||
fireEvent.click(screen.getByLabelText('发送'))
|
||||
await waitFor(() => expect(input).toHaveValue(''))
|
||||
expect(measureHeight).toHaveBeenCalledTimes(1)
|
||||
expect(input).toHaveStyle({ height: '72px' })
|
||||
})
|
||||
|
||||
it('shows a red recording state until microphone capture stops', async () => {
|
||||
let resolveRecording!: (value: {
|
||||
audio: ArrayBuffer
|
||||
@@ -3325,9 +3360,12 @@ describe('App', () => {
|
||||
scrollTop: { configurable: true, writable: true, value: 225 }
|
||||
})
|
||||
fireEvent.scroll(firstChat)
|
||||
messageRenderProbe.mockClear()
|
||||
fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
|
||||
target: { value: '第一段会话草稿' }
|
||||
})
|
||||
expect(messageRenderProbe).not.toHaveBeenCalled()
|
||||
expect(firstPane?.querySelectorAll('.message')).toHaveLength(160)
|
||||
fireEvent.click(screen.getByLabelText('添加附件'))
|
||||
expect(
|
||||
await screen.findByText(draftAttachment.name)
|
||||
@@ -3341,9 +3379,12 @@ describe('App', () => {
|
||||
expect(
|
||||
screen.queryByText(draftAttachment.name)
|
||||
).not.toBeInTheDocument()
|
||||
messageRenderProbe.mockClear()
|
||||
fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
|
||||
target: { value: '第二段会话草稿' }
|
||||
})
|
||||
expect(messageRenderProbe).not.toHaveBeenCalled()
|
||||
expect(firstPane?.querySelectorAll('.message')).toHaveLength(160)
|
||||
fireEvent.click(
|
||||
screen.getByText('第一段长会话').closest('button')!
|
||||
)
|
||||
|
||||
+23
-15
@@ -931,11 +931,23 @@ function ChatHistoryPane({
|
||||
0,
|
||||
conversation.messages.length - visibleMessageCount
|
||||
)
|
||||
const visibleMessages = conversation.messages.slice(
|
||||
visibleMessageStartIndex
|
||||
const visibleMessages = useMemo(
|
||||
() => conversation.messages.slice(visibleMessageStartIndex),
|
||||
[conversation.messages, visibleMessageStartIndex]
|
||||
)
|
||||
const hiddenMessageCount = visibleMessageStartIndex
|
||||
|
||||
const handleArticleRef = useCallback(
|
||||
(messageId: string, element: HTMLElement | null): void => {
|
||||
if (element) {
|
||||
messageArticleRefs.current.set(messageId, element)
|
||||
} else {
|
||||
messageArticleRefs.current.delete(messageId)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const saveScrollPosition = useCallback(
|
||||
(scrollContainer: HTMLElement): boolean => {
|
||||
const distanceFromBottom =
|
||||
@@ -1068,7 +1080,7 @@ function ChatHistoryPane({
|
||||
}
|
||||
}, [visibleMessageCount])
|
||||
|
||||
const revealEarlierMessages = (): void => {
|
||||
const revealEarlierMessages = useCallback((): void => {
|
||||
const scrollContainer = scrollRef.current
|
||||
if (scrollContainer) {
|
||||
prependScrollPositionRef.current = {
|
||||
@@ -1087,7 +1099,12 @@ function ChatHistoryPane({
|
||||
conversation.id,
|
||||
visibleMessageCount + messageRenderBatchSize
|
||||
)
|
||||
}
|
||||
}, [
|
||||
conversation.id,
|
||||
conversation.messages,
|
||||
onVisibleMessageCountChange,
|
||||
visibleMessageCount
|
||||
])
|
||||
|
||||
const scrollToBottom = (): void => {
|
||||
const scrollContainer = scrollRef.current
|
||||
@@ -1156,13 +1173,7 @@ function ChatHistoryPane({
|
||||
locale={locale}
|
||||
messages={visibleMessages}
|
||||
messageStartIndex={visibleMessageStartIndex}
|
||||
onArticleRef={(messageId, element) => {
|
||||
if (element) {
|
||||
messageArticleRefs.current.set(messageId, element)
|
||||
} else {
|
||||
messageArticleRefs.current.delete(messageId)
|
||||
}
|
||||
}}
|
||||
onArticleRef={handleArticleRef}
|
||||
onCopyMessage={onCopyMessage}
|
||||
onDownloadImage={onDownloadImage}
|
||||
onOpenCitationContext={onOpenCitationContext}
|
||||
@@ -3073,7 +3084,7 @@ function App(): React.JSX.Element {
|
||||
assistantTasksRef.current = assistantTasks
|
||||
}, [assistantTasks])
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
resizeComposerTextarea(inputRef.current)
|
||||
}, [input])
|
||||
|
||||
@@ -9679,9 +9690,6 @@ function App(): React.JSX.Element {
|
||||
title={t('composer.keyboardHint')}
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
onInput={(event) =>
|
||||
resizeComposerTextarea(event.currentTarget)
|
||||
}
|
||||
onPaste={(event) => {
|
||||
const imageItem = Array.from(
|
||||
event.clipboardData.items
|
||||
|
||||
@@ -182,6 +182,12 @@ describe('ChatTimeline', () => {
|
||||
unchangedDetails!.open = true
|
||||
markdownRenderProbe.mockClear()
|
||||
|
||||
const mapMessages = vi.spyOn(messages, 'map')
|
||||
rerender(<ChatTimeline {...props} messages={messages} />)
|
||||
expect(mapMessages).not.toHaveBeenCalled()
|
||||
expect(markdownRenderProbe).not.toHaveBeenCalled()
|
||||
mapMessages.mockRestore()
|
||||
|
||||
const streamedMessages = messages.map((message, index) =>
|
||||
index === messages.length - 1
|
||||
? { ...message, content: `${message.content} delta` }
|
||||
|
||||
@@ -1163,7 +1163,7 @@ type ChatTimelineProps = {
|
||||
totalMessageCount: number
|
||||
}
|
||||
|
||||
export function ChatTimeline({
|
||||
export const ChatTimeline = memo(function ChatTimeline({
|
||||
artifactById,
|
||||
conversationId,
|
||||
hiddenMessageCount,
|
||||
@@ -1230,4 +1230,4 @@ export function ChatTimeline({
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user