diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index 61702d9d..2ad1c575 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -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() + return { + ...original, + MarkdownRenderer: (props: { children: string; renderHtml?: boolean }) => { + messageRenderProbe(props.children) + return + } + } +}) + const lazyRouteMocks = vi.hoisted(() => { let pending: Promise | 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() + 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')! ) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 95b3aaf9..54ae8a41 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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 diff --git a/src/renderer/src/ChatTimeline.test.tsx b/src/renderer/src/ChatTimeline.test.tsx index 1c3a6547..0736d852 100644 --- a/src/renderer/src/ChatTimeline.test.tsx +++ b/src/renderer/src/ChatTimeline.test.tsx @@ -182,6 +182,12 @@ describe('ChatTimeline', () => { unchangedDetails!.open = true markdownRenderProbe.mockClear() + const mapMessages = vi.spyOn(messages, 'map') + rerender() + 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` } diff --git a/src/renderer/src/ChatTimeline.tsx b/src/renderer/src/ChatTimeline.tsx index 65d77670..e05a038e 100644 --- a/src/renderer/src/ChatTimeline.tsx +++ b/src/renderer/src/ChatTimeline.tsx @@ -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({ })} ) -} +})