66import React , { useState , useCallback , useRef , useEffect , useMemo } from 'react' ;
77import { withConversationExcerptFallback } from '@/shared/utils/conversationExcerpt' ;
88import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost' ;
9- import { RotateCcw , Loader2 } from 'lucide-react' ;
9+ import { CircleAlert , RotateCcw , Loader2 } from 'lucide-react' ;
1010import type { DialogTurn , FlowUserSteeringItem } from '../../types/flow-chat' ;
1111import type { ImageContext } from '@/shared/types/context' ;
1212import { flowChatManager } from '../../services/FlowChatManager' ;
@@ -27,7 +27,7 @@ import { globalEventBus } from '@/infrastructure/event-bus';
2727import { shouldIgnoreCardToggleClick } from '@/shared/utils/textSelection' ;
2828import { observeElementResize } from '@/shared/utils/sharedResizeObserver' ;
2929import { formatContextForPrompt } from '@/shared/utils/contextPrompt' ;
30- import { Dialog , DialogClose , Tooltip , Icon , IconButton } from '@openbitfun/ui' ;
30+ import { Button , Dialog , DialogClose , Tooltip , Icon , IconButton } from '@openbitfun/ui' ;
3131import { confirmDanger } from '@/infrastructure/confirm-dialog' ;
3232import { ToolProcessingDots } from '@openbitfun/ui/flow-chat' ;
3333import { UserMessageEditComposer } from './UserMessageEditComposer' ;
@@ -62,6 +62,8 @@ import { buildImagePayload } from '../../utils/imagePayload';
6262import { UserMessagePresentationContent , UserMessageTextContent } from './UserMessagePresentationContent' ;
6363import { UserMessageImage } from './UserMessageImage' ;
6464import { useSubmittedMessageMotion } from './useSubmittedMessageMotion' ;
65+ import { finishSubmittedMessagePreview } from '../../services/submittedMessagePresentation' ;
66+ import { getActiveSurfaceScope } from '@/infrastructure/peer-device/deviceSurface' ;
6567import './UserMessageItem.scss' ;
6668
6769const log = createLogger ( 'UserMessageItem' ) ;
@@ -72,6 +74,8 @@ interface UserMessageItemProps {
7274 absoluteTurnIndex ?: number ;
7375 turnStatus ?: DialogTurn [ 'status' ] ;
7476 steeringStatus ?: FlowUserSteeringItem [ 'status' ] ;
77+ submissionPhase ?: 'forming' | 'failed' ;
78+ submissionError ?: string ;
7579}
7680
7781function buildPresentationRerunPayload ( presentation : ComposerPresentation ) : {
@@ -104,7 +108,7 @@ function buildPresentationRerunPayload(presentation: ComposerPresentation): {
104108}
105109
106110export const UserMessageItem = React . memo < UserMessageItemProps > (
107- ( { message, turnId, absoluteTurnIndex, turnStatus, steeringStatus } ) => {
111+ ( { message, turnId, absoluteTurnIndex, turnStatus, steeringStatus, submissionPhase , submissionError } ) => {
108112 const { t, formatDate } = useI18n ( 'flow-chat' ) ;
109113 const {
110114 sessionId,
@@ -198,7 +202,8 @@ export const UserMessageItem = React.memo<UserMessageItemProps>(
198202 const resolvedTurnStatus = dialogTurn ?. status ?? turnStatus ;
199203 const isFailed = resolvedTurnStatus === 'error' ;
200204 const resolvedSessionId = sessionId ?? currentSession ?. sessionId ;
201- useSubmittedMessageMotion ( shellRef , resolvedSessionId , turnId , message ?. id , isFailed || isEditing ) ;
205+ useSubmittedMessageMotion ( shellRef , resolvedSessionId , turnId , message ?. id ,
206+ isFailed || isEditing || submissionPhase === 'failed' , submissionPhase ) ;
202207 const sessionMachine = useSessionStateMachine ( resolvedSessionId ?? null ) ;
203208 const sessionExecutionState = sessionMachine && sessionMachine . sessionId === resolvedSessionId
204209 ? sessionMachine . currentState
@@ -498,7 +503,10 @@ export const UserMessageItem = React.memo<UserMessageItemProps>(
498503 contexts : restoredComposerContexts ,
499504 ...( composerPresentation ? { composerPresentation } : { } ) ,
500505 } ) ;
501- } , [ composerPresentation , messageContent , restoredComposerContexts ] ) ;
506+ if ( submissionPhase === 'failed' && resolvedSessionId ) {
507+ finishSubmittedMessagePreview ( getActiveSurfaceScope ( ) , resolvedSessionId , turnId ) ;
508+ }
509+ } , [ composerPresentation , messageContent , restoredComposerContexts , submissionPhase , resolvedSessionId , turnId ] ) ;
502510
503511 const handleOpenUsageReport = useCallback ( ( report : SessionUsageReport , initialTab ?: SessionUsagePanelTab ) => {
504512 void import ( '../../services/openSessionUsageReport' ) . then ( ( { openSessionUsagePanel } ) => {
@@ -579,14 +587,15 @@ export const UserMessageItem = React.memo<UserMessageItemProps>(
579587 < div
580588 data-openbitfun-product-component = "user-message-item"
581589 data-openbitfun-product-part = "root"
582- data-openbitfun-state = { [ expanded && 'expanded' , isFailed && 'failed' ] . filter ( Boolean ) . join ( ' ' ) || undefined }
590+ data-openbitfun-state = { [ expanded && 'expanded' , isFailed && 'failed' , submissionPhase ] . filter ( Boolean ) . join ( ' ' ) || undefined }
583591 ref = { containerRef }
584- className = { `user-message-item ${ expanded ? 'user-message-item--expanded' : '' } ${ isFailed ? ' user-message-item--failed' : '' } ${ isEditing ? ' user-message-item--editing' : '' } ` }
592+ className = { `user-message-item ${ expanded ? 'user-message-item--expanded' : '' } ${ isFailed ? ' user-message-item--failed' : '' } ${ isEditing ? ' user-message-item--editing' : '' } ${ submissionPhase === 'forming' ? ' user-message-item--forming' : '' } ${ submissionPhase === 'failed' ? ' user-message-item--submission-failed' : '' } ` }
585593 data-testid = "chat-user-message"
586594 data-turn-id = { turnId }
587- data-status = { resolvedTurnStatus || '' }
588- data-failed = { isFailed ? 'true' : 'false' }
595+ data-status = { submissionPhase === 'failed' ? 'error' : resolvedTurnStatus || '' }
596+ data-failed = { isFailed || submissionPhase === 'failed' ? 'true' : 'false' }
589597 >
598+ < span className = "user-message-item__surface" aria-hidden = "true" />
590599 { isEditing ? (
591600 < div className = "user-message-item__edit-layout" >
592601 < UserMessageEditComposer
@@ -699,7 +708,16 @@ export const UserMessageItem = React.memo<UserMessageItemProps>(
699708 ) }
700709 </ div >
701710
702- < div className = "user-message-item__meta" data-openbitfun-product-component = "user-message-item" data-openbitfun-product-part = "meta" >
711+ { submissionPhase === 'failed' && (
712+ < div className = "user-message-item__submission-error" role = "alert" >
713+ < Icon glyph = { CircleAlert } size = "sm" className = "user-message-item__submission-error-icon" />
714+ < span title = { submissionError } > { t ( 'error.sendFailed' ) } </ span >
715+ < Button type = "button" variant = "outline" size = "sm" onClick = { handleFillToInput } >
716+ { t ( 'message.continueEditing' ) }
717+ </ Button >
718+ </ div >
719+ ) }
720+ { ! submissionPhase && < div className = "user-message-item__meta" data-openbitfun-product-component = "user-message-item" data-openbitfun-product-part = "meta" >
703721 { ! isEditing && sentTime && sentAtLabel && sentTimestamp !== null && (
704722 < time
705723 className = "user-message-item__timestamp"
@@ -762,7 +780,7 @@ export const UserMessageItem = React.memo<UserMessageItemProps>(
762780 ) : null }
763781 </ div >
764782 ) }
765- </ div >
783+ </ div > }
766784
767785 </ div >
768786 ) ;
0 commit comments