Skip to content

Commit b4d69f9

Browse files
committed
refactor: extract footer logic into MessageFooter component
- Create new MessageFooter component with loading timer and completion footer - Move all footer-related logic (copy, time, credits, feedback) to MessageFooter - Simplify MessageBlock by removing ~170 lines of footer code - Remove unused isErrorButtonHovered state - Better separation of concerns: MessageBlock focuses on content, MessageFooter handles utilities
1 parent 36ee12b commit b4d69f9

File tree

2 files changed

+222
-183
lines changed

2 files changed

+222
-183
lines changed

cli/src/components/message-block.tsx

Lines changed: 13 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,10 @@ import React, { memo, useCallback, useMemo, useState, type ReactNode } from 'rea
44

55
import { AgentBranchItem } from './agent-branch-item'
66
import { 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'
108
import { ValidationErrorPopover } from './validation-error-popover'
119
import { useTheme } from '../hooks/use-theme'
1210
import { useWhyDidYouUpdateById } from '../hooks/use-why-did-you-update'
13-
import {
14-
useFeedbackStore,
15-
selectIsFeedbackOpenForMessage,
16-
selectHasSubmittedFeedback,
17-
selectMessageFeedbackCategory,
18-
} from '../state/feedback-store'
1911
import { isTextBlock, isToolBlock } from '../types/chat'
2012
import { shouldRenderAsSimpleText } from '../utils/constants'
2113
import {
@@ -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

Comments
 (0)