import { memo, useState, useCallback, useEffect } from 'react' import type { ReactNode } from 'react' import { cn } from '@/lib/utils' import { motion, AnimatePresence } from 'framer-motion' import { Tooltip } from '../basic/tooltip' import { IconButton } from '../basic/icon-button' import { ArrowUpLine, CloseLine, ArrowDownSLine } from '../basic/icons-inline' import { Scrollbar } from '../basic/scrollbar' export interface QueueIndicatorItem { id: string label: ReactNode attachmentCount?: number } export interface QueueIndicatorLabels { title?: string sendNowTip?: string removeTip?: string fileLabel?: string filesLabel?: string } const DEFAULT_LABELS: Required = { title: 'In queue', sendNowTip: 'Send now', removeTip: 'Remove', fileLabel: 'file', filesLabel: 'files', } export interface QueueIndicatorProps { items: QueueIndicatorItem[] onRemoveItem?: (itemId: string) => void onSendNow?: (itemId: string) => void isStreaming?: boolean hasStatusCardBelow?: boolean labels?: QueueIndicatorLabels defaultExpanded?: boolean storageKey?: string arrowUpIcon?: ReactNode closeIcon?: ReactNode arrowDownSIcon?: ReactNode } const arrowUpClass = 'w-3.5 h-3.5' const closeClass = 'w-3.5 h-3.5' const arrowDownClass = 'w-4 h-4 text-text-tertiary transition-transform duration-200' const QueueItemRow = memo(function QueueItemRow({ item, onRemove, onSendNow, labels, arrowUpIcon, closeIcon, }: { item: QueueIndicatorItem onRemove?: (itemId: string) => void onSendNow?: (itemId: string) => void labels: Required arrowUpIcon: ReactNode closeIcon: ReactNode }) { const handleRemove = useCallback( (e: React.MouseEvent) => { e.stopPropagation() onRemove?.(item.id) }, [item.id, onRemove] ) const handleSendNow = useCallback( (e: React.MouseEvent) => { e.stopPropagation() onSendNow?.(item.id) }, [item.id, onSendNow] ) const attachmentCount = item.attachmentCount ?? 0 const fileWord = attachmentCount === 1 ? labels.fileLabel : labels.filesLabel return (
{item.label} {attachmentCount > 0 && ( +{attachmentCount} {fileWord} )}
{onSendNow && ( )} {onRemove && ( )}
) }) export const QueueIndicator = memo(function QueueIndicator({ items, onRemoveItem, onSendNow, isStreaming = false, hasStatusCardBelow = false, labels: labelsProp, defaultExpanded = true, storageKey, arrowUpIcon, closeIcon, arrowDownSIcon, }: QueueIndicatorProps) { void isStreaming const labels = { ...DEFAULT_LABELS, ...labelsProp } const defaultArrowUp = arrowUpIcon ?? const defaultClose = closeIcon ?? const defaultArrowDown = arrowDownSIcon ?? const [isExpanded, setIsExpanded] = useState(() => { if (typeof window === 'undefined') return defaultExpanded if (storageKey) { const saved = localStorage.getItem(storageKey) return saved !== null ? saved === 'true' : defaultExpanded } return defaultExpanded }) useEffect(() => { if (storageKey) { localStorage.setItem(storageKey, String(isExpanded)) } }, [storageKey, isExpanded]) if (items.length === 0) return null const containerStyles = cn( 'w-full overflow-hidden flex flex-col', 'border border-b-0 border-border border-border-tertiary bg-bg-base', !hasStatusCardBelow && 'rounded-t-xl' ) return (
setIsExpanded(!isExpanded)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() setIsExpanded(!isExpanded) } }} aria-expanded={isExpanded} aria-label={`${isExpanded ? 'Collapse' : 'Expand'} queue`} className="flex items-center justify-between px-3 py-1 h-7 cursor-pointer bg-fill-tertiary hover:bg-fill-secondary transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-border" >
{defaultArrowDown} {labels.title}
{isExpanded && ( {items.map((item) => ( ))} )}
) }) QueueIndicator.displayName = 'QueueIndicator'