@@ -4,18 +4,10 @@ import React, { memo, useCallback, useMemo, useState, type ReactNode } from 'rea
44
55import { AgentBranchItem } from './agent-branch-item'
66import { Button } from './button'
7- import { CopyIconButton } from './copy-icon-button'
8- import { ElapsedTimer } from './elapsed-timer'
9- import { FeedbackIconButton } from './feedback-icon-button'
7+ import { MessageFooter } from './message-footer'
108import { ValidationErrorPopover } from './validation-error-popover'
119import { useTheme } from '../hooks/use-theme'
1210import { useWhyDidYouUpdateById } from '../hooks/use-why-did-you-update'
13- import {
14- useFeedbackStore ,
15- selectIsFeedbackOpenForMessage ,
16- selectHasSubmittedFeedback ,
17- selectMessageFeedbackCategory ,
18- } from '../state/feedback-store'
1911import { isTextBlock , isToolBlock } from '../types/chat'
2012import { shouldRenderAsSimpleText } from '../utils/constants'
2113import {
@@ -98,7 +90,6 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
9890 onOpenFeedback,
9991} ) => {
10092 const [ showValidationPopover , setShowValidationPopover ] = useState ( false )
101- const [ isErrorButtonHovered , setIsErrorButtonHovered ] = useState ( false )
10293
10394 useWhyDidYouUpdateById (
10495 'MessageBlock' ,
@@ -136,174 +127,7 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
136127 )
137128
138129 const theme = useTheme ( )
139-
140- // Memoize selectors to prevent new function references on every render
141- const selectIsFeedbackOpenMemo = useMemo (
142- ( ) => selectIsFeedbackOpenForMessage ( messageId ) ,
143- [ messageId ] ,
144- )
145- const selectHasSubmittedFeedbackMemo = useMemo (
146- ( ) => selectHasSubmittedFeedback ( messageId ) ,
147- [ messageId ] ,
148- )
149- const selectMessageFeedbackCategoryMemo = useMemo (
150- ( ) => selectMessageFeedbackCategory ( messageId ) ,
151- [ messageId ] ,
152- )
153-
154- const isFeedbackOpen = useFeedbackStore ( selectIsFeedbackOpenMemo )
155- const hasSubmittedFeedback = useFeedbackStore ( selectHasSubmittedFeedbackMemo )
156- const selectedFeedbackCategory = useFeedbackStore (
157- selectMessageFeedbackCategoryMemo ,
158- )
159-
160130 const resolvedTextColor = textColor ?? theme . foreground
161- const shouldShowLoadingTimer = isAi && isLoading && ! isComplete
162- const shouldShowCompletionFooter = isAi && isComplete
163- const canRequestFeedback = shouldShowCompletionFooter && ! hasSubmittedFeedback
164- const isGoodOrBadSelection =
165- selectedFeedbackCategory === 'good_result' ||
166- selectedFeedbackCategory === 'bad_result'
167- const shouldShowSubmittedFeedbackState =
168- shouldShowCompletionFooter && hasSubmittedFeedback && isGoodOrBadSelection
169- const shouldRenderFeedbackButton =
170- Boolean ( onFeedback ) &&
171- ( canRequestFeedback || shouldShowSubmittedFeedbackState )
172-
173- const handleFeedbackOpen = useCallback ( ( ) => {
174- if ( ! canRequestFeedback || ! onFeedback ) return
175- onFeedback ( messageId )
176- } , [ canRequestFeedback , onFeedback , messageId ] )
177-
178- const handleFeedbackClose = useCallback ( ( ) => {
179- if ( ! canRequestFeedback ) return
180- onCloseFeedback ?.( )
181- } , [ canRequestFeedback , onCloseFeedback ] )
182-
183- const renderLoadingTimer = ( ) => {
184- if ( ! shouldShowLoadingTimer ) {
185- return null
186- }
187- return (
188- < text
189- attributes = { TextAttributes . DIM }
190- style = { {
191- wrapMode : 'none' ,
192- marginTop : 0 ,
193- marginBottom : 0 ,
194- alignSelf : 'flex-end' ,
195- } }
196- >
197- < ElapsedTimer
198- startTime = { timerStartTime }
199- attributes = { TextAttributes . DIM }
200- />
201- </ text >
202- )
203- }
204-
205- const renderCompletionFooter = ( ) => {
206- if ( ! shouldShowCompletionFooter ) {
207- return null
208- }
209-
210- const footerItems : { key : string ; node : React . ReactNode } [ ] = [ ]
211-
212- // Add copy button first if there's content to copy
213- const hasContent = ( blocks && blocks . length > 0 ) || ( content && content . trim ( ) . length > 0 )
214- if ( hasContent ) {
215- footerItems . push ( {
216- key : 'copy' ,
217- node : < CopyIconButton blocks = { blocks } content = { content } /> ,
218- } )
219- }
220-
221- if ( completionTime ) {
222- footerItems . push ( {
223- key : 'time' ,
224- node : (
225- < text
226- attributes = { TextAttributes . DIM }
227- style = { {
228- wrapMode : 'none' ,
229- fg : theme . secondary ,
230- marginTop : 0 ,
231- marginBottom : 0 ,
232- } }
233- >
234- { completionTime }
235- </ text >
236- ) ,
237- } )
238- }
239- if ( typeof credits === 'number' && credits > 0 ) {
240- footerItems . push ( {
241- key : 'credits' ,
242- node : (
243- < text
244- attributes = { TextAttributes . DIM }
245- style = { {
246- wrapMode : 'none' ,
247- fg : theme . secondary ,
248- marginTop : 0 ,
249- marginBottom : 0 ,
250- } }
251- >
252- { pluralize ( credits , 'credit' ) }
253- </ text >
254- ) ,
255- } )
256- }
257- if ( shouldRenderFeedbackButton ) {
258- footerItems . push ( {
259- key : 'feedback' ,
260- node : (
261- < FeedbackIconButton
262- onClick = { handleFeedbackOpen }
263- onClose = { handleFeedbackClose }
264- isOpen = { canRequestFeedback ? isFeedbackOpen : false }
265- messageId = { messageId }
266- selectedCategory = { selectedFeedbackCategory }
267- hasSubmittedFeedback = { hasSubmittedFeedback }
268- />
269- ) ,
270- } )
271- }
272-
273- if ( footerItems . length === 0 ) {
274- return null
275- }
276-
277- return (
278- < box
279- style = { {
280- flexDirection : 'row' ,
281- alignItems : 'center' ,
282- alignSelf : 'flex-end' ,
283- gap : 1 ,
284- } }
285- >
286- { footerItems . map ( ( item , idx ) => (
287- < React . Fragment key = { item . key } >
288- { idx > 0 && (
289- < text
290- attributes = { TextAttributes . DIM }
291- style = { {
292- wrapMode : 'none' ,
293- fg : theme . muted ,
294- marginTop : 0 ,
295- marginBottom : 0 ,
296- } }
297- >
298- •
299- </ text >
300- ) }
301- { item . node }
302- </ React . Fragment >
303- ) ) }
304- </ box >
305- )
306- }
307131
308132 return (
309133 < box
@@ -328,8 +152,6 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
328152 { validationErrors && validationErrors . length > 0 && (
329153 < Button
330154 onClick = { ( ) => setShowValidationPopover ( ! showValidationPopover ) }
331- onMouseOver = { ( ) => setIsErrorButtonHovered ( true ) }
332- onMouseOut = { ( ) => setIsErrorButtonHovered ( false ) }
333155 >
334156 < text
335157 style = { {
@@ -385,10 +207,18 @@ export const MessageBlock: React.FC<MessageBlockProps> = ({
385207 />
386208 ) }
387209 { isAi && (
388- < >
389- { renderLoadingTimer ( ) }
390- { renderCompletionFooter ( ) }
391- </ >
210+ < MessageFooter
211+ messageId = { messageId }
212+ blocks = { blocks }
213+ content = { content }
214+ isLoading = { isLoading }
215+ isComplete = { isComplete }
216+ completionTime = { completionTime }
217+ credits = { credits }
218+ timerStartTime = { timerStartTime }
219+ onFeedback = { onFeedback }
220+ onCloseFeedback = { onCloseFeedback }
221+ />
392222 ) }
393223 </ box >
394224 )
0 commit comments