11import { pendingQueueManager } from '../services/flow-chat-manager/PendingQueueModule' ;
22import type { QueuedMessage } from '../types/flow-chat' ;
3- import { useEffect , useState , useSyncExternalStore } from 'react' ;
3+ import { useEffect , useId , useState , useSyncExternalStore } from 'react' ;
44import { useI18n } from '@/infrastructure/i18n' ;
5- import { Button } from '@openbitfun/ui' ;
5+ import { Button , IconButton , OverflowText , Tooltip } from '@openbitfun/ui' ;
6+ import { ArrowUp , ChevronDown , ChevronUp , Info , Pencil , RotateCw , X } from 'lucide-react' ;
7+ import './HostPendingQueuePanel.scss' ;
68import { HostDialogQueue , observeHostQueue , type QueueOutboxRecord , type QueueItem } from '../../../../shared/dialog-queue/HostDialogQueue' ;
79import {
810 ChatComposerQueue , ChatComposerQueueHeader , ChatComposerQueueTitle ,
@@ -12,6 +14,9 @@ import {
1214export function HostPendingQueuePanel ( { queue, onRestore } : { queue : HostDialogQueue ; onRestore : ( item : QueuedMessage ) => boolean } ) {
1315 const { t } = useI18n ( 'flow-chat' ) ;
1416 const view = useSyncExternalStore ( queue . subscribe , queue . getSnapshot , queue . getSnapshot ) ;
17+ const [ expanded , setExpanded ] = useState ( true ) ;
18+ const [ showHelp , setShowHelp ] = useState ( false ) ;
19+ const listId = useId ( ) ;
1520 const [ busy , setBusy ] = useState ( false ) ;
1621 const [ error , setError ] = useState < string | null > ( null ) ;
1722 useEffect ( ( ) => observeHostQueue ( queue ) , [ queue ] ) ;
@@ -47,37 +52,44 @@ export function HostPendingQueuePanel({ queue, onRestore }: { queue: HostDialogQ
4752 const items = view . snapshot ?. items ?? [ ] ;
4853 const visibleError = error || ( view . error ?. includes ( 'Session is not loaded' ) ? null : view . error ) ;
4954 if ( ! items . length && ! view . pending . length && ! visibleError ) return null ;
50- return < ChatComposerQueue aria-label = { t ( 'hostQueue.title' ) } >
51- < ChatComposerQueueHeader > < ChatComposerQueueTitle count = { items . length } > { t ( 'hostQueue.title' ) } </ ChatComposerQueueTitle > </ ChatComposerQueueHeader >
52- < p > { t ( 'hostQueue.memoryNotice' ) } </ p >
53- { visibleError && < div role = "alert" > { visibleError } < Button size = "sm" disabled = { busy } onClick = { ( ) => void run ( ( ) => queue . refresh ( ) ) } > { t ( 'hostQueue.refresh' ) } </ Button > </ div > }
54- < ChatComposerQueueList >
55- { items . map ( item => < ChatComposerQueueItem key = { item . turnId } state = { item . status === 'blocked' ? 'failed' : item . status === 'steering_pending' ? 'sending' : 'default' } >
56- < ChatComposerQueueItemContent >
57- < div > { item . displayContent } </ div >
58- < span > { item . status === 'blocked' ? t ( 'hostQueue.blocked' ) : item . status === 'steering_pending' ? t ( 'hostQueue.steeringPending' ) : t ( 'hostQueue.queued' ) } </ span >
59- { item . attachmentCount > 0 && < span > { t ( 'hostQueue.attachments' , { count : item . attachmentCount } ) } </ span > }
60- { item . reason && < p > { item . reason } </ p > }
55+ return < ChatComposerQueue data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "root" className = "host-pending-queue" aria-label = { t ( 'hostQueue.title' ) } >
56+ < ChatComposerQueueHeader data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "header" >
57+ < Button variant = "text" size = "xs" className = "host-pending-queue__toggle" aria-expanded = { expanded } aria-controls = { listId }
58+ onClick = { ( ) => setExpanded ( value => ! value ) } trailingIcon = { expanded ? < ChevronDown size = { 14 } /> : < ChevronUp size = { 14 } /> } >
59+ < ChatComposerQueueTitle data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "title" count = { items . length + view . pending . length } > { t ( 'hostQueue.title' ) } </ ChatComposerQueueTitle >
60+ </ Button >
61+ < Tooltip content = { t ( 'hostQueue.about' ) } > < IconButton size = "xs" aria-label = { t ( 'hostQueue.about' ) } aria-expanded = { showHelp }
62+ icon = { < Info size = { 14 } /> } onClick = { ( ) => setShowHelp ( value => ! value ) } /> </ Tooltip >
63+ </ ChatComposerQueueHeader >
64+ { showHelp && < p data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "status" className = "host-pending-queue__note" > { t ( 'hostQueue.memoryNotice' ) } </ p > }
65+ { visibleError && < div data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "status" role = "alert" className = "host-pending-queue__error" > { visibleError } < Button size = "xs" variant = "text" disabled = { busy } onClick = { ( ) => void run ( ( ) => queue . refresh ( ) ) } > { t ( 'hostQueue.refresh' ) } </ Button > </ div > }
66+ < ChatComposerQueueList data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "list" id = { listId } hidden = { ! expanded } >
67+ { items . map ( item => < ChatComposerQueueItem data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "item" key = { item . turnId } state = { item . status === 'blocked' ? 'failed' : item . status === 'steering_pending' ? 'sending' : 'default' } >
68+ < ChatComposerQueueItemContent data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "content" >
69+ < OverflowText title = { item . displayContent } > { item . displayContent || t ( 'pendingQueue.emptyPlaceholder' ) } </ OverflowText >
70+ { item . status !== 'queued' && < span className = "host-pending-queue__status" > { item . status === 'blocked' ? t ( 'hostQueue.blocked' ) : t ( 'hostQueue.steeringPending' ) } </ span > }
71+ { item . attachmentCount > 0 && < span className = "host-pending-queue__status" > { t ( 'hostQueue.attachments' , { count : item . attachmentCount } ) } </ span > }
72+ { item . reason && < OverflowText className = "host-pending-queue__status" title = { item . reason } > { item . reason } </ OverflowText > }
6173 </ ChatComposerQueueItemContent >
62- < ChatComposerQueueItemActions >
63- < Button size = "sm" disabled = { busy || ! ! view . error || item . status === 'steering_pending' } onClick = { ( ) => void run ( async ( ) => {
74+ < ChatComposerQueueItemActions data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "actions" >
75+ < Tooltip content = { t ( 'hostQueue.edit' ) } > < IconButton size = "xs" aria-label = { t ( 'hostQueue.edit' ) } icon = { < Pencil size = { 14 } /> } disabled = { busy || ! ! view . error || item . status === 'steering_pending' } onClick = { ( ) => void run ( async ( ) => {
6476 const saved = await queue . savedDraft ( item . turnId ) ;
6577 if ( ! saved || saved . request . action !== 'submit' ) throw new Error ( t ( 'hostQueue.noDraft' ) ) ;
6678 await restoreDraft ( saved , item ) ;
67- } ) } > { t ( 'hostQueue.edit' ) } </ Button >
68- < Button size = "sm" disabled = { busy || ! ! view . error || item . status === 'steering_pending' } onClick = { ( ) => void run ( ( ) => queue . act ( item , 'promote' ) ) } > { t ( 'hostQueue.sendNow' ) } </ Button >
69- < Button size = "sm" disabled = { busy || ! ! view . error || item . status === 'steering_pending' } onClick = { ( ) => void run ( ( ) => queue . act ( item , 'cancel' ) ) } > { t ( 'hostQueue.cancel' ) } </ Button >
79+ } ) } /> </ Tooltip >
80+ < Tooltip content = { t ( 'hostQueue.sendNow' ) } > < IconButton size = "xs" aria-label = { t ( 'hostQueue.sendNow' ) } icon = { < ArrowUp size = { 14 } /> } disabled = { busy || ! ! view . error || item . status === 'steering_pending' } onClick = { ( ) => void run ( ( ) => queue . act ( item , 'promote' ) ) } /> </ Tooltip >
81+ < Tooltip content = { t ( 'hostQueue.cancel' ) } > < IconButton size = "xs" aria-label = { t ( 'hostQueue.cancel' ) } icon = { < X size = { 14 } /> } disabled = { busy || ! ! view . error || item . status === 'steering_pending' } onClick = { ( ) => void run ( ( ) => queue . act ( item , 'cancel' ) ) } /> </ Tooltip >
7082 </ ChatComposerQueueItemActions >
7183 </ ChatComposerQueueItem > ) }
72- { view . pending . map ( record => < ChatComposerQueueItem key = { record . key } state = "failed" >
73- < ChatComposerQueueItemContent > { t ( 'hostQueue.unknown' ) }
74- { record . request . action === 'submit' && < div > { record . request . message . displayContent ?? record . request . message . content } </ div > }
84+ { view . pending . map ( record => < ChatComposerQueueItem data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "item" key = { record . key } state = "failed" >
85+ < ChatComposerQueueItemContent data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "content" > < OverflowText className = "host-pending-queue__status" title = { t ( 'hostQueue.unknown' ) } > { t ( 'hostQueue.unknown' ) } </ OverflowText >
86+ { record . request . action === 'submit' && < OverflowText title = { record . request . message . displayContent ?? record . request . message . content } > { record . request . message . displayContent ?? record . request . message . content } </ OverflowText > }
7587 </ ChatComposerQueueItemContent >
76- < ChatComposerQueueItemActions >
88+ < ChatComposerQueueItemActions data-openbitfun-product-component = "pending-queue-panel" data-openbitfun-product-part = "actions" >
7789 { record . restoreIntent && record . request . action === 'submit'
78- ? < Button size = "sm" disabled = { busy } onClick = { ( ) => void run ( ( ) => restoreDraft ( record ) ) } > { t ( 'hostQueue.edit' ) } </ Button >
79- : < Button size = "sm" disabled = { busy } onClick = { ( ) => void run ( ( ) => queue . retry ( record ) ) } > { t ( 'hostQueue.checkRetry' ) } </ Button > }
80- < Button size = "sm" disabled = { busy } onClick = { ( ) => void run ( ( ) => queue . dismiss ( record ) ) } > { t ( 'hostQueue.dismiss' ) } </ Button >
90+ ? < IconButton size = "xs" aria-label = { t ( 'hostQueue.edit' ) } title = { t ( 'hostQueue.edit' ) } icon = { < Pencil size = { 14 } /> } disabled = { busy } onClick = { ( ) => void run ( ( ) => restoreDraft ( record ) ) } / >
91+ : < IconButton size = "xs" aria-label = { t ( 'hostQueue.checkRetry' ) } title = { t ( 'hostQueue.checkRetry' ) } icon = { < RotateCw size = { 14 } /> } disabled = { busy } onClick = { ( ) => void run ( ( ) => queue . retry ( record ) ) } / >}
92+ < IconButton size = "xs" aria-label = { t ( 'hostQueue.dismiss' ) } title = { t ( 'hostQueue.dismiss' ) } icon = { < X size = { 14 } /> } disabled = { busy } onClick = { ( ) => void run ( ( ) => queue . dismiss ( record ) ) } / >
8193 </ ChatComposerQueueItemActions >
8294 </ ChatComposerQueueItem > ) }
8395 </ ChatComposerQueueList >
0 commit comments