@@ -16,6 +16,7 @@ import { ConversationMessage } from '../../models/ConversationMessage';
1616import { ConversationParticipant } from '../../models/ConversationParticipant' ;
1717import { useUpdateConversationParticipantMutation } from '../../services/workbench' ;
1818import { MemoizedInteractMessage } from './Message/InteractMessage' ;
19+ import { MemoizedNotificationAccordion } from './Message/NotificationAccordion' ;
1920import { MemoizedToolResultMessage } from './Message/ToolResultMessage' ;
2021import { ParticipantStatus } from './ParticipantStatus' ;
2122
@@ -132,95 +133,155 @@ export const InteractHistory: React.FC<InteractHistoryProps> = (props) => {
132133
133134 // create a list of memoized interact message components for rendering in the virtuoso component
134135 React . useEffect ( ( ) => {
135- let lastMessageInfo = {
136- participantId : '' ,
137- attribution : undefined as string | undefined ,
138- time : undefined as dayjs . Dayjs | undefined ,
139- } ;
140136 let lastDate = '' ;
141137 let generatedResponseCount = 0 ;
142138
143- const updatedItems = messages
144- . filter ( ( message ) => message . messageType !== 'log' )
145- . map ( ( message , index ) => {
146- // if a hash is provided, check if the message id matches the hash
147- if ( hash === `#${ message . id } ` ) {
148- // set the hash item index to scroll to the item
149- setScrollToIndex ( index ) ;
150- }
139+ // Filter out log messages first
140+ const filteredMessages = messages . filter ( ( message ) => message . messageType !== 'log' ) ;
151141
152- const senderParticipant = participants . find (
153- ( participant ) => participant . id === message . sender . participantId ,
154- ) ;
155- if ( ! senderParticipant ) {
156- // if the sender participant is not found, do not render the message.
157- // this can happen temporarily if the provided conversation was just
158- // re-retrieved, but the participants have not been re-retrieved yet
159- return (
160- < div key = { message . id } className = { classes . item } >
161- Participant not found: { message . sender . participantId } in conversation { conversation . id }
142+ // Group sequential notification messages
143+ const groupedItems : React . ReactNode [ ] = [ ] ;
144+ let currentNotificationGroup : ConversationMessage [ ] = [ ] ;
145+ let lastNotificationTime : dayjs . Dayjs | undefined ;
146+
147+ const finishCurrentGroup = ( insertIndex ?: number ) => {
148+ if ( currentNotificationGroup . length > 0 ) {
149+ if ( currentNotificationGroup . length === 1 ) {
150+ // Single notification - render normally
151+ const message = currentNotificationGroup [ 0 ] ;
152+ const item = renderSingleMessage ( message , insertIndex ) ;
153+ if ( item ) groupedItems . push ( item ) ;
154+ } else {
155+ // Multiple notifications - render as accordion
156+ const accordionItem = (
157+ < div className = { classes . item } key = { `accordion-${ currentNotificationGroup [ 0 ] . id } ` } >
158+ < MemoizedNotificationAccordion
159+ messages = { currentNotificationGroup }
160+ participants = { participants }
161+ conversation = { conversation }
162+ readOnly = { readOnly }
163+ onRead = { handleOnRead }
164+ onVisible = { handleOnVisible }
165+ onRewindToBefore = { onRewindToBefore }
166+ />
162167 </ div >
163168 ) ;
169+ groupedItems . push ( accordionItem ) ;
164170 }
171+ currentNotificationGroup = [ ] ;
172+ }
173+ } ;
165174
166- const date = Utility . toFormattedDateString ( message . timestamp , 'M/D/YY' ) ;
167- let displayDate = false ;
168- if ( date !== lastDate ) {
169- displayDate = true ;
170- lastDate = date ;
171- }
172-
173- if (
174- message . messageType === 'chat' &&
175- message . sender . participantRole !== 'user' &&
176- message . metadata ?. generated_content !== false
177- ) {
178- generatedResponseCount += 1 ;
179- }
180-
181- // avoid duplicate header for messages from the same participant, if the
182- // attribution is the same and the message is within a minute of the last
183- let hideParticipant = message . messageType !== 'chat' ;
184- const messageTime = dayjs . utc ( message . timestamp ) ;
185- if (
186- lastMessageInfo . participantId === senderParticipant . id &&
187- lastMessageInfo . attribution === message . metadata ?. attribution &&
188- messageTime . diff ( lastMessageInfo . time , 'minute' ) < 1
189- ) {
190- hideParticipant = true ;
191- }
192- lastMessageInfo = {
193- participantId : senderParticipant . id ,
194- attribution : message . metadata ?. attribution ,
195- time : messageTime ,
196- } ;
197-
198- // FIXME: add new message type in workbench service/app for tool results
199- const isToolResult = message . messageType === 'note' && message . metadata ?. [ 'tool_result' ] ;
200-
201- // Use memoized message components to prevent re-rendering all messages when one changes
202- const messageContent = isToolResult ? (
203- < MemoizedToolResultMessage conversation = { conversation } message = { message } readOnly = { readOnly } />
204- ) : (
205- < MemoizedInteractMessage
206- readOnly = { readOnly }
207- conversation = { conversation }
208- message = { message }
209- participant = { senderParticipant }
210- hideParticipant = { hideParticipant }
211- displayDate = { displayDate }
212- onRead = { handleOnRead }
213- onVisible = { handleOnVisible }
214- onRewind = { onRewindToBefore }
215- />
216- ) ;
175+ const renderSingleMessage = ( message : ConversationMessage , originalIndex ?: number ) => {
176+ // if a hash is provided, check if the message id matches the hash
177+ if ( hash === `#${ message . id } ` ) {
178+ // set the hash item index to scroll to the item
179+ setScrollToIndex ( originalIndex ?? groupedItems . length ) ;
180+ }
217181
182+ const senderParticipant = participants . find (
183+ ( participant ) => participant . id === message . sender . participantId ,
184+ ) ;
185+ if ( ! senderParticipant ) {
186+ // if the sender participant is not found, do not render the message.
187+ // this can happen temporarily if the provided conversation was just
188+ // re-retrieved, but the participants have not been re-retrieved yet
218189 return (
219- < div className = { classes . item } key = { message . id } >
220- { messageContent }
190+ < div key = { message . id } className = { classes . item } >
191+ Participant not found: { message . sender . participantId } in conversation { conversation . id }
221192 </ div >
222193 ) ;
223- } ) ;
194+ }
195+
196+ const date = Utility . toFormattedDateString ( message . timestamp , 'M/D/YY' ) ;
197+ let displayDate = false ;
198+ if ( date !== lastDate ) {
199+ displayDate = true ;
200+ lastDate = date ;
201+ }
202+
203+ if (
204+ message . messageType === 'chat' &&
205+ message . sender . participantRole !== 'user' &&
206+ message . metadata ?. generated_content !== false
207+ ) {
208+ generatedResponseCount += 1 ;
209+ }
210+
211+ // FIXME: add new message type in workbench service/app for tool results
212+ const isToolResult = message . messageType === 'note' && message . metadata ?. [ 'tool_result' ] ;
213+
214+ // Use memoized message components to prevent re-rendering all messages when one changes
215+ const messageContent = isToolResult ? (
216+ < MemoizedToolResultMessage conversation = { conversation } message = { message } readOnly = { readOnly } />
217+ ) : (
218+ < MemoizedInteractMessage
219+ readOnly = { readOnly }
220+ conversation = { conversation }
221+ message = { message }
222+ participant = { senderParticipant }
223+ hideParticipant = { false } // Single messages show participant
224+ displayDate = { displayDate }
225+ onRead = { handleOnRead }
226+ onVisible = { handleOnVisible }
227+ onRewind = { onRewindToBefore }
228+ />
229+ ) ;
230+
231+ return (
232+ < div className = { classes . item } key = { message . id } >
233+ { messageContent }
234+ </ div >
235+ ) ;
236+ } ;
237+
238+ filteredMessages . forEach ( ( message , index ) => {
239+ const senderParticipant = participants . find (
240+ ( participant ) => participant . id === message . sender . participantId ,
241+ ) ;
242+
243+ // Skip if participant not found
244+ if ( ! senderParticipant ) {
245+ finishCurrentGroup ( index ) ;
246+ const errorItem = renderSingleMessage ( message , index ) ;
247+ if ( errorItem ) groupedItems . push ( errorItem ) ;
248+ return ;
249+ }
250+
251+ const isNotification = message . messageType !== 'chat' ;
252+ const messageTime = dayjs . utc ( message . timestamp ) ;
253+
254+ if ( isNotification ) {
255+ // Check if this notification should be grouped with the previous ones
256+ // Group all sequential notifications within 1 minute regardless of sender
257+ const shouldGroup = currentNotificationGroup . length > 0 &&
258+ lastNotificationTime &&
259+ messageTime . diff ( lastNotificationTime , 'minute' ) < 1 ;
260+
261+ if ( shouldGroup ) {
262+ // Add to current group
263+ currentNotificationGroup . push ( message ) ;
264+ } else {
265+ // Finish current group and start new one
266+ finishCurrentGroup ( index ) ;
267+ currentNotificationGroup = [ message ] ;
268+ }
269+
270+ lastNotificationTime = messageTime ;
271+ } else {
272+ // Chat message - finish any current notification group
273+ finishCurrentGroup ( index ) ;
274+
275+ // Render chat message normally
276+ const item = renderSingleMessage ( message , index ) ;
277+ if ( item ) groupedItems . push ( item ) ;
278+ }
279+ } ) ;
280+
281+ // Finish any remaining notification group
282+ finishCurrentGroup ( ) ;
283+
284+ const updatedItems = groupedItems ;
224285
225286 if ( generatedResponseCount > 0 ) {
226287 updatedItems . push (
0 commit comments