11'use client'
22
3- import { useLayoutEffect , useRef } from 'react'
3+ import { memo , useCallback , useEffect , useLayoutEffect , useMemo , useRef } from 'react'
44import { cn } from '@/lib/core/utils/cn'
55import { MessageActions } from '@/app/workspace/[workspaceId]/components'
66import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
@@ -17,6 +17,9 @@ import {
1717import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content'
1818import type {
1919 ChatMessage ,
20+ ChatMessageAttachment ,
21+ ChatMessageContext ,
22+ ContentBlock ,
2023 FileAttachmentForApi ,
2124 MothershipResource ,
2225 QueuedMessage ,
@@ -78,6 +81,100 @@ const LAYOUT_STYLES = {
7881 } ,
7982} as const
8083
84+ const EMPTY_BLOCKS : ContentBlock [ ] = [ ]
85+
86+ interface UserMessageRowProps {
87+ content : string
88+ contexts ?: ChatMessageContext [ ]
89+ attachments ?: ChatMessageAttachment [ ]
90+ rowClassName : string
91+ bubbleClassName : string
92+ attachmentWidthClassName : string
93+ }
94+
95+ const UserMessageRow = memo ( function UserMessageRow ( {
96+ content,
97+ contexts,
98+ attachments,
99+ rowClassName,
100+ bubbleClassName,
101+ attachmentWidthClassName,
102+ } : UserMessageRowProps ) {
103+ const hasAttachments = Boolean ( attachments ?. length )
104+ return (
105+ < div className = { rowClassName } >
106+ { hasAttachments && (
107+ < ChatMessageAttachments
108+ attachments = { attachments ?? [ ] }
109+ align = 'end'
110+ className = { attachmentWidthClassName }
111+ />
112+ ) }
113+ < div className = { bubbleClassName } >
114+ < UserMessageContent content = { content } contexts = { contexts } />
115+ </ div >
116+ </ div >
117+ )
118+ } )
119+
120+ interface AssistantMessageRowProps {
121+ message : ChatMessage
122+ isStreaming : boolean
123+ precedingUserContent ?: string
124+ chatId ?: string
125+ rowClassName : string
126+ onOptionSelect ?: ( id : string ) => void
127+ onWorkspaceResourceSelect ?: ( resource : MothershipResource ) => void
128+ }
129+
130+ const AssistantMessageRow = memo ( function AssistantMessageRow ( {
131+ message,
132+ isStreaming,
133+ precedingUserContent,
134+ chatId,
135+ rowClassName,
136+ onOptionSelect,
137+ onWorkspaceResourceSelect,
138+ } : AssistantMessageRowProps ) {
139+ const blocks = message . contentBlocks ?? EMPTY_BLOCKS
140+ const hasAnyBlocks = blocks . length > 0
141+ const trimmedContent = message . content ?. trim ( ) ?? ''
142+
143+ if ( ! hasAnyBlocks && ! trimmedContent && isStreaming ) {
144+ return < PendingTagIndicator />
145+ }
146+
147+ const hasRenderableAssistant = assistantMessageHasRenderableContent ( blocks , message . content ?? '' )
148+ if ( ! hasRenderableAssistant && ! trimmedContent && ! isStreaming ) {
149+ return null
150+ }
151+
152+ const showActions = ! isStreaming && ( message . content || hasAnyBlocks )
153+
154+ return (
155+ < div className = { rowClassName } >
156+ < MessageContent
157+ blocks = { blocks }
158+ fallbackContent = { message . content }
159+ isStreaming = { isStreaming }
160+ onOptionSelect = { onOptionSelect }
161+ onWorkspaceResourceSelect = { onWorkspaceResourceSelect }
162+ />
163+ { showActions && (
164+ < div className = 'mt-2.5' >
165+ < MessageActions
166+ content = { message . content }
167+ chatId = { chatId }
168+ userQuery = { precedingUserContent }
169+ requestId = { message . requestId }
170+ messageId = { message . id }
171+ />
172+ </ div >
173+ ) }
174+ </ div >
175+ )
176+ } )
177+
81178export function MothershipChat ( {
82179 messages,
83180 isSending,
@@ -111,17 +208,31 @@ export function MothershipChat({
111208 const { staged : stagedMessages , isStaging } = useProgressiveList ( messages , stagingKey )
112209 const stagedMessageCount = stagedMessages . length
113210 const stagedOffset = messages . length - stagedMessages . length
114- const precedingUserContentByIndex : Array < string | undefined > = [ ]
115- let lastUserContent : string | undefined
116- for ( const [ index , message ] of messages . entries ( ) ) {
117- precedingUserContentByIndex [ index ] = lastUserContent
118- if ( message . role === 'user' ) {
119- lastUserContent = message . content
211+ const precedingUserContentByIndex = useMemo ( ( ) => {
212+ const out : Array < string | undefined > = [ ]
213+ let lastUserContent : string | undefined
214+ for ( const [ index , message ] of messages . entries ( ) ) {
215+ out [ index ] = lastUserContent
216+ if ( message . role === 'user' ) lastUserContent = message . content
120217 }
121- }
218+ return out
219+ } , [ messages ] )
122220 const initialScrollDoneRef = useRef ( false )
123221 const userInputRef = useRef < UserInputHandle > ( null )
124222
223+ const onSubmitRef = useRef ( onSubmit )
224+ const onWorkspaceResourceSelectRef = useRef ( onWorkspaceResourceSelect )
225+ useEffect ( ( ) => {
226+ onSubmitRef . current = onSubmit
227+ onWorkspaceResourceSelectRef . current = onWorkspaceResourceSelect
228+ } , [ onSubmit , onWorkspaceResourceSelect ] )
229+ const stableOnOptionSelect = useCallback ( ( id : string ) => {
230+ onSubmitRef . current ( id )
231+ } , [ ] )
232+ const stableOnWorkspaceResourceSelect = useCallback ( ( resource : MothershipResource ) => {
233+ onWorkspaceResourceSelectRef . current ?.( resource )
234+ } , [ ] )
235+
125236 function handleSendQueuedHead ( ) {
126237 const topMessage = messageQueue [ 0 ]
127238 if ( ! topMessage ) return
@@ -164,63 +275,31 @@ export function MothershipChat({
164275 { stagedMessages . map ( ( msg , localIndex ) => {
165276 const index = stagedOffset + localIndex
166277 if ( msg . role === 'user' ) {
167- const hasAttachments = Boolean ( msg . attachments ?. length )
168278 return (
169- < div key = { msg . id } className = { styles . userRow } >
170- { hasAttachments && (
171- < ChatMessageAttachments
172- attachments = { msg . attachments ?? [ ] }
173- align = 'end'
174- className = { styles . attachmentWidth }
175- />
176- ) }
177- < div className = { styles . userBubble } >
178- < UserMessageContent content = { msg . content } contexts = { msg . contexts } />
179- </ div >
180- </ div >
279+ < UserMessageRow
280+ key = { msg . id }
281+ content = { msg . content }
282+ contexts = { msg . contexts }
283+ attachments = { msg . attachments }
284+ rowClassName = { styles . userRow }
285+ bubbleClassName = { styles . userBubble }
286+ attachmentWidthClassName = { styles . attachmentWidth }
287+ />
181288 )
182289 }
183290
184- const hasAnyBlocks = Boolean ( msg . contentBlocks ?. length )
185- const hasRenderableAssistant = assistantMessageHasRenderableContent (
186- msg . contentBlocks ?? [ ] ,
187- msg . content ?? ''
188- )
189- const isLastAssistant = index === messages . length - 1
190- const isThisStreaming = isStreamActive && isLastAssistant
191-
192- if ( ! hasAnyBlocks && ! msg . content ?. trim ( ) && isThisStreaming ) {
193- return < PendingTagIndicator key = { msg . id } />
194- }
195-
196- if ( ! hasRenderableAssistant && ! msg . content ?. trim ( ) && ! isThisStreaming ) {
197- return null
198- }
199-
200- const isLastMessage = index === messages . length - 1
201- const precedingUserContent = precedingUserContentByIndex [ index ]
202-
291+ const isLast = index === messages . length - 1
203292 return (
204- < div key = { msg . id } className = { styles . assistantRow } >
205- < MessageContent
206- blocks = { msg . contentBlocks || [ ] }
207- fallbackContent = { msg . content }
208- isStreaming = { isThisStreaming }
209- onOptionSelect = { isLastMessage ? onSubmit : undefined }
210- onWorkspaceResourceSelect = { onWorkspaceResourceSelect }
211- />
212- { ! isThisStreaming && ( msg . content || msg . contentBlocks ?. length ) && (
213- < div className = 'mt-2.5' >
214- < MessageActions
215- content = { msg . content }
216- chatId = { chatId }
217- userQuery = { precedingUserContent }
218- requestId = { msg . requestId }
219- messageId = { msg . id }
220- />
221- </ div >
222- ) }
223- </ div >
293+ < AssistantMessageRow
294+ key = { msg . id }
295+ message = { msg }
296+ isStreaming = { isStreamActive && isLast }
297+ precedingUserContent = { precedingUserContentByIndex [ index ] }
298+ chatId = { chatId }
299+ rowClassName = { styles . assistantRow }
300+ onOptionSelect = { isLast ? stableOnOptionSelect : undefined }
301+ onWorkspaceResourceSelect = { stableOnWorkspaceResourceSelect }
302+ />
224303 )
225304 } ) }
226305 </ div >
0 commit comments