11
2- import React , { useState , useCallback , useMemo } from 'react' ;
3- import { ChatMessage , UploadedFile , AppSettings , SideViewContent , VideoMetadata } from '../../types' ;
2+ import React , { useMemo } from 'react' ;
3+ import { ChatMessage , AppSettings , SideViewContent , VideoMetadata } from '../../types' ;
44import { Message } from '../message/Message' ;
55import { translations } from '../../utils/appUtils' ;
66import { HtmlPreviewModal } from '../modals/HtmlPreviewModal' ;
7- import { FilePreviewModal } from '../shared/ImageZoomModal' ;
8- import { ThemeColors } from '../../constants/themeConstants' ;
9- import { SUPPORTED_IMAGE_MIME_TYPES } from '../../constants/fileConstants' ;
7+ import { FilePreviewModal } from '../modals/FilePreviewModal' ;
8+ import { ThemeColors } from '../../types/theme' ;
109import { WelcomeScreen } from './message-list/WelcomeScreen' ;
1110import { MessageListPlaceholder } from './message-list/MessageListPlaceholder' ;
1211import { ScrollNavigation } from './message-list/ScrollNavigation' ;
1312import { FileConfigurationModal } from '../modals/FileConfigurationModal' ;
1413import { MediaResolution } from '../../types/settings' ;
1514import { isGemini3Model } from '../../utils/appUtils' ;
16- import { TextSelectionToolbar } from './TextSelectionToolbar' ;
15+ import { TextSelectionToolbar } from './message-list/TextSelectionToolbar' ;
16+ import { useMessageListUI } from '../../hooks/useMessageListUI' ;
1717
1818export interface MessageListProps {
1919 messages : ChatMessage [ ] ;
2020 sessionTitle ?: string ;
2121 scrollContainerRef : React . RefObject < HTMLDivElement > ;
2222 setScrollContainerRef : ( node : HTMLDivElement | null ) => void ;
2323 onScrollContainerScroll : ( ) => void ;
24- onEditMessage : ( messageId : string ) => void ;
24+ onEditMessage : ( messageId : string , mode ?: 'update' | 'resend' ) => void ;
2525 onDeleteMessage : ( messageId : string ) => void ;
2626 onRetryMessage : ( messageId : string ) => void ;
27- onEditMessageContent : ( message : ChatMessage ) => void ;
2827 onUpdateMessageFile : ( messageId : string , fileId : string , updates : { videoMetadata ?: VideoMetadata , mediaResolution ?: MediaResolution } ) => void ;
2928 showThoughts : boolean ;
3029 themeColors : ThemeColors ;
@@ -37,6 +36,7 @@ export interface MessageListProps {
3736 onOrganizeInfoClick ?: ( suggestion : string ) => void ;
3837 onFollowUpSuggestionClick ?: ( suggestion : string ) => void ;
3938 onTextToSpeech : ( messageId : string , text : string ) => void ;
39+ onGenerateCanvas : ( messageId : string , text : string ) => void ;
4040 ttsMessageId : string | null ;
4141 t : ( key : keyof typeof translations , fallback ?: string ) => string ;
4242 language : 'en' | 'zh' ;
@@ -52,108 +52,32 @@ export interface MessageListProps {
5252
5353export const MessageList : React . FC < MessageListProps > = ( {
5454 messages, sessionTitle, scrollContainerRef, setScrollContainerRef, onScrollContainerScroll,
55- onEditMessage, onDeleteMessage, onRetryMessage, onEditMessageContent , onUpdateMessageFile, showThoughts, themeColors, baseFontSize,
56- expandCodeBlocksByDefault, isMermaidRenderingEnabled, isGraphvizRenderingEnabled, onSuggestionClick, onOrganizeInfoClick, onFollowUpSuggestionClick, onTextToSpeech, ttsMessageId, t, language, themeId,
55+ onEditMessage, onDeleteMessage, onRetryMessage, onUpdateMessageFile, showThoughts, themeColors, baseFontSize,
56+ expandCodeBlocksByDefault, isMermaidRenderingEnabled, isGraphvizRenderingEnabled, onSuggestionClick, onOrganizeInfoClick, onFollowUpSuggestionClick, onTextToSpeech, onGenerateCanvas , ttsMessageId, t, language, themeId,
5757 scrollNavVisibility, onScrollToPrevTurn, onScrollToNextTurn,
5858 chatInputHeight, appSettings, currentModelId, onOpenSidePanel, onQuote
5959} ) => {
60- const [ previewFile , setPreviewFile ] = useState < UploadedFile | null > ( null ) ;
61-
62- const [ isHtmlPreviewModalOpen , setIsHtmlPreviewModalOpen ] = useState ( false ) ;
63- const [ htmlToPreview , setHtmlToPreview ] = useState < string | null > ( null ) ;
64- const [ initialTrueFullscreenRequest , setInitialTrueFullscreenRequest ] = useState ( false ) ;
65-
66- // File Configuration State
67- const [ configuringFile , setConfiguringFile ] = useState < { file : UploadedFile , messageId : string } | null > ( null ) ;
68-
69- // Virtualization state
70- const [ visibleMessages , setVisibleMessages ] = useState < Set < string > > ( ( ) => {
71- const initialVisible = new Set < string > ( ) ;
72- const lastN = 15 ;
73- for ( let i = Math . max ( 0 , messages . length - lastN ) ; i < messages . length ; i ++ ) {
74- initialVisible . add ( messages [ i ] . id ) ;
75- }
76- return initialVisible ;
77- } ) ;
78-
79- const estimateMessageHeight = useCallback ( ( message : ChatMessage ) => {
80- if ( ! message ) return 150 ;
81- let height = 80 ;
82- if ( message . content ) {
83- const lines = message . content . length / 80 ;
84- height += lines * 24 ;
85- }
86- if ( message . files && message . files . length > 0 ) {
87- height += message . files . length * 120 ;
88- }
89- if ( message . thoughts && showThoughts ) {
90- height += 100 ;
91- }
92- return Math . min ( height , 1200 ) ;
93- } , [ showThoughts ] ) ;
94-
95- const handleBecameVisible = useCallback ( ( messageId : string ) => {
96- setVisibleMessages ( prev => {
97- if ( prev . has ( messageId ) ) return prev ;
98- const newSet = new Set ( prev ) ;
99- newSet . add ( messageId ) ;
100- return newSet ;
101- } ) ;
102- } , [ ] ) ;
103-
104- const handleFileClick = useCallback ( ( file : UploadedFile ) => {
105- setPreviewFile ( file ) ;
106- } , [ ] ) ;
107-
108- const closeFilePreviewModal = useCallback ( ( ) => {
109- setPreviewFile ( null ) ;
110- } , [ ] ) ;
111-
112- const allImages = useMemo ( ( ) => {
113- if ( ! previewFile ) return [ ] ;
114- return messages . flatMap ( m => m . files || [ ] ) . filter ( f =>
115- ( SUPPORTED_IMAGE_MIME_TYPES . includes ( f . type ) || f . type === 'image/svg+xml' ) && ! f . error
116- ) ;
117- } , [ messages , previewFile ] ) ;
118-
119- const currentImageIndex = useMemo ( ( ) => {
120- if ( ! previewFile ) return - 1 ;
121- return allImages . findIndex ( f => f . id === previewFile . id ) ;
122- } , [ allImages , previewFile ] ) ;
123-
124- const handlePrevImage = useCallback ( ( ) => {
125- if ( currentImageIndex > 0 ) {
126- setPreviewFile ( allImages [ currentImageIndex - 1 ] ) ;
127- }
128- } , [ currentImageIndex , allImages ] ) ;
129-
130- const handleNextImage = useCallback ( ( ) => {
131- if ( currentImageIndex < allImages . length - 1 ) {
132- setPreviewFile ( allImages [ currentImageIndex + 1 ] ) ;
133- }
134- } , [ currentImageIndex , allImages ] ) ;
135-
136- const handleOpenHtmlPreview = useCallback ( ( htmlContent : string , options ?: { initialTrueFullscreen ?: boolean } ) => {
137- setHtmlToPreview ( htmlContent ) ;
138- setInitialTrueFullscreenRequest ( options ?. initialTrueFullscreen ?? false ) ;
139- setIsHtmlPreviewModalOpen ( true ) ;
140- } , [ ] ) ;
141-
142- const handleCloseHtmlPreview = useCallback ( ( ) => {
143- setIsHtmlPreviewModalOpen ( false ) ;
144- setHtmlToPreview ( null ) ;
145- setInitialTrueFullscreenRequest ( false ) ;
146- } , [ ] ) ;
147-
148- const handleConfigureFile = useCallback ( ( file : UploadedFile , messageId : string ) => {
149- setConfiguringFile ( { file, messageId } ) ;
150- } , [ ] ) ;
151-
152- const handleSaveFileConfig = useCallback ( ( fileId : string , updates : { videoMetadata ?: VideoMetadata , mediaResolution ?: MediaResolution } ) => {
153- if ( configuringFile ) {
154- onUpdateMessageFile ( configuringFile . messageId , fileId , updates ) ;
155- }
156- } , [ configuringFile , onUpdateMessageFile ] ) ;
60+ const {
61+ previewFile,
62+ isHtmlPreviewModalOpen,
63+ htmlToPreview,
64+ initialTrueFullscreenRequest,
65+ configuringFile,
66+ setConfiguringFile,
67+ visibleMessages,
68+ handleBecameVisible,
69+ handleFileClick,
70+ closeFilePreviewModal,
71+ allImages,
72+ currentImageIndex,
73+ handlePrevImage,
74+ handleNextImage,
75+ handleOpenHtmlPreview,
76+ handleCloseHtmlPreview,
77+ handleConfigureFile,
78+ handleSaveFileConfig,
79+ estimateMessageHeight
80+ } = useMessageListUI ( { messages, onUpdateMessageFile } ) ;
15781
15882 // Determine if current model is Gemini 3 to enable per-part resolution
15983 const isGemini3 = useMemo ( ( ) => {
@@ -193,7 +117,6 @@ export const MessageList: React.FC<MessageListProps> = ({
193117 onEditMessage = { onEditMessage }
194118 onDeleteMessage = { onDeleteMessage }
195119 onRetryMessage = { onRetryMessage }
196- onEditMessageContent = { onEditMessageContent }
197120 onImageClick = { handleFileClick }
198121 onOpenHtmlPreview = { handleOpenHtmlPreview }
199122 showThoughts = { showThoughts }
@@ -204,6 +127,7 @@ export const MessageList: React.FC<MessageListProps> = ({
204127 isMermaidRenderingEnabled = { isMermaidRenderingEnabled }
205128 isGraphvizRenderingEnabled = { isGraphvizRenderingEnabled }
206129 onTextToSpeech = { onTextToSpeech }
130+ onGenerateCanvas = { onGenerateCanvas }
207131 ttsMessageId = { ttsMessageId }
208132 onSuggestionClick = { onFollowUpSuggestionClick }
209133 t = { t }
@@ -217,7 +141,7 @@ export const MessageList: React.FC<MessageListProps> = ({
217141 return (
218142 < MessageListPlaceholder
219143 key = { `${ msg . id } -placeholder` }
220- height = { estimateMessageHeight ( msg ) }
144+ height = { estimateMessageHeight ( msg , showThoughts ) }
221145 onVisible = { ( ) => handleBecameVisible ( msg . id ) }
222146 />
223147 ) ;
0 commit comments