import React, { useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react' import type { AttachedFile } from './types' import { Button, ButtonSize, ButtonSubType, ButtonType } from '../Button' import { Menu, type MenuProps } from '../Menu' import { Tag, TagColor, TagSize, TagVariant } from '../Tags' import { Tooltip } from '../Tooltip' import Block from '../Primitives/Block/Block' import { capitalizeFirstLetter } from '../../global-utils/GlobalUtils' import { getDocIcon } from './getDocIcon' import { Plus, X } from 'lucide-react' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import { ChatInputTokensType } from './chatInput.tokens' import { createOverflowMenuItems } from './utils' import { useResizeObserver } from '../../hooks/useResizeObserver' import { useDebounce } from '../../hooks/useDebounce' type AttachmentFileProps = { attachedFiles: AttachedFile[] onFileRemove?: (fileId: string) => void onFileClick?: (file: AttachedFile) => void overflowMenuProps?: Partial containerRef?: React.RefObject } const AttachmentFile = ({ attachedFiles, onFileRemove, onFileClick, overflowMenuProps, containerRef, }: AttachmentFileProps) => { const [cutOffIndex, setCutOffIndex] = useState(attachedFiles.length) const [overflowMenuOpen, setOverflowMenuOpen] = useState(false) const tokens = useResponsiveTokens('CHAT_INPUT') const isExpanding = useRef(false) const filesContainerRef = useRef(null) const generatedId = useId() const filesRegionId = `chat-input-files-${generatedId}` const lastWidth = useRef(0) const visibleFiles = useMemo(() => { return attachedFiles.slice(0, cutOffIndex) }, [attachedFiles, cutOffIndex]) const hiddenFiles = useMemo(() => { return attachedFiles.slice(cutOffIndex) }, [attachedFiles, cutOffIndex]) const overflowMenuItems = useMemo(() => { return createOverflowMenuItems( hiddenFiles, onFileRemove || (() => {}), onFileClick || (() => {}) ) }, [hiddenFiles, onFileRemove, onFileClick]) const hasOverflow = hiddenFiles.length > 0 // Dynamic overflow calculation const handleResize = useCallback(() => { if (!filesContainerRef.current || attachedFiles.length === 0) return const container = filesContainerRef.current const containerRect = container.getBoundingClientRect() const containerWidth = containerRect.width const BUFFER = 30 const MORE_BUTTON_ESTIMATED_WIDTH = 100 const GAP_SIZE = 8 // from tokens.filesContainer.gap const fileItems = Array.from(container.children).filter( (child) => !child.classList.contains('overflow-menu-trigger') ) if (isExpanding.current && fileItems.length < attachedFiles.length) { setCutOffIndex(attachedFiles.length) isExpanding.current = false setTimeout(() => { handleResize() }, 50) return } let totalWidth = 0 let optimalCutoff = 0 for ( let i = 0; i < Math.min(fileItems.length, attachedFiles.length); i++ ) { const itemWidth = ( fileItems[i] as HTMLElement ).getBoundingClientRect().width const totalGaps = i > 0 ? i * GAP_SIZE : 0 const remainingItems = attachedFiles.length - (i + 1) const needsMoreButton = remainingItems > 0 const requiredSpace = totalWidth + itemWidth + totalGaps + (needsMoreButton ? MORE_BUTTON_ESTIMATED_WIDTH + GAP_SIZE : 0) + BUFFER if (requiredSpace <= containerWidth) { totalWidth += itemWidth optimalCutoff = i + 1 } else { break } } const newCutoff = Math.max( 1, Math.min(optimalCutoff, attachedFiles.length) ) setCutOffIndex(newCutoff) }, [attachedFiles.length]) const debouncedResize = useDebounce(handleResize, 150) // Reset cutoff when files change useEffect(() => { setCutOffIndex(attachedFiles.length) isExpanding.current = false const timeoutId = setTimeout(() => { handleResize() }, 150) return () => clearTimeout(timeoutId) }, [attachedFiles.length, handleResize]) // Watch for container width changes useResizeObserver( containerRef as React.RefObject, ({ width }) => { if (!lastWidth || !width) return const currentWidth = lastWidth.current ?? 0 if (Math.abs(width - currentWidth) > 10) { if (width > currentWidth + 20) { isExpanding.current = true } lastWidth.current = width debouncedResize() } } ) return ( attachedFiles.length > 0 && ( {visibleFiles.map((file) => { const fileName = file.name || capitalizeFirstLetter(file.type) return ( ) })} {hasOverflow && ( } text={`${hiddenFiles.length} more`} aria-label={`Show ${hiddenFiles.length} more attached file${ hiddenFiles.length !== 1 ? 's' : '' }`} > {hiddenFiles.length} more } items={[ { items: overflowMenuItems, }, ]} open={overflowMenuOpen} onOpenChange={setOverflowMenuOpen} {...overflowMenuProps} /> )} ) ) } export default AttachmentFile