import React, { createContext, useCallback, useContext, useMemo, type ComponentProps, type ReactNode, } from 'react'; import { Platform } from 'react-native'; import { ScrollView } from 'react-native'; import { Box } from '../box'; import { Button } from '../button'; import { Image } from '../image'; import { Tooltip, TooltipContent, TooltipText } from '../tooltip'; import type { FileUIPart, SourceDocumentUIPart } from 'ai'; import { FileText, Globe, Image as ImageIcon, Music2, Paperclip, Video, X, } from 'lucide-react-native'; export type AttachmentData = | (FileUIPart & { id: string }) | (SourceDocumentUIPart & { id: string }); export type AttachmentMediaCategory = | 'image' | 'video' | 'audio' | 'document' | 'source' | 'unknown'; export type AttachmentVariant = 'grid' | 'inline' | 'list'; export const getMediaCategory = ( data: AttachmentData ): AttachmentMediaCategory => { if (data.type === 'source-document') return 'source'; const mediaType = data.mediaType ?? ''; if (mediaType.startsWith('image/')) return 'image'; if (mediaType.startsWith('video/')) return 'video'; if (mediaType.startsWith('audio/')) return 'audio'; if (mediaType.startsWith('application/') || mediaType.startsWith('text/')) return 'document'; return 'unknown'; }; export const getAttachmentLabel = (data: AttachmentData): string => { if (data.type === 'source-document') { return data.title || data.filename || 'Source'; } const category = getMediaCategory(data); return data.filename || (category === 'image' ? 'Image' : 'Attachment'); }; interface AttachmentsContextValue { variant: AttachmentVariant; } const AttachmentsContext = createContext(null); interface AttachmentContextValue { data: AttachmentData; mediaCategory: AttachmentMediaCategory; onRemove?: () => void; variant: AttachmentVariant; } const AttachmentContext = createContext(null); export const useAttachmentsContext = () => useContext(AttachmentsContext) ?? { variant: 'grid' as const }; export const useAttachmentContext = () => { const ctx = useContext(AttachmentContext); if (!ctx) throw new Error('Attachment components must be used within '); return ctx; }; export type AttachmentsProps = ComponentProps & { variant?: AttachmentVariant; }; export const Attachments = ({ variant = 'grid', className = '', children, ...props }: AttachmentsProps) => { const contextValue = useMemo(() => ({ variant }), [variant]); return ( {children} ); }; export type AttachmentProps = ComponentProps & { data: AttachmentData; onRemove?: () => void; }; export const Attachment = ({ data, onRemove, className = '', children, ...props }: AttachmentProps) => { const { variant } = useAttachmentsContext(); const mediaCategory = getMediaCategory(data); const contextValue = useMemo( () => ({ data, mediaCategory, onRemove, variant }), [data, mediaCategory, onRemove, variant] ); return ( {children} {onRemove && ( )} ); }; export type AttachmentPreviewProps = ComponentProps & { fallbackIcon?: ReactNode; }; export const AttachmentPreview = ({ fallbackIcon, className = '', ...props }: AttachmentPreviewProps) => { const { data, mediaCategory, variant } = useAttachmentContext(); const renderContent = () => { if (mediaCategory === 'image' && data.type === 'file' && data.url) { return ( {data.filename ); } const iconMap: Record> = { audio: Music2, document: FileText, image: ImageIcon, source: Globe, unknown: Paperclip, video: Video, }; const Icon = iconMap[mediaCategory]; const size = variant === 'inline' ? 12 : variant === 'list' ? 24 : 20; return ( fallbackIcon ?? ); }; return ( {renderContent()} ); }; export type AttachmentRemoveProps = ComponentProps & { label?: string; }; export const AttachmentRemove = ({ label = 'Remove', className = '', children, ...props }: AttachmentRemoveProps) => { const { onRemove, variant } = useAttachmentContext(); const isWeb = Platform.OS === 'web'; const handlePress = useCallback(() => onRemove?.(), [onRemove]); if (!onRemove) return null; return ( ); }; export type AttachmentHoverCardProps = Omit< ComponentProps, 'trigger' | 'children' > & { children: ReactNode; }; export const AttachmentHoverCard = ({ children, placement = 'top', openDelay = 0, closeDelay = 100, ...props }: AttachmentHoverCardProps) => { const childrenArray = React.Children.toArray(children); const triggerElement = childrenArray[0]; const contentElements = childrenArray.slice(1); if (!triggerElement) { return <>{children}; } return ( { if (React.isValidElement(triggerElement)) { return React.cloneElement(triggerElement, { ...triggerProps, ...triggerElement.props, }); } return triggerElement; }} {...props} > {contentElements} ); }; export const AttachmentHoverCardContent = ({ className = '', children, ...props }: ComponentProps) => ( {children} ); export const AttachmentHoverCardText = TooltipText; export const AttachmentEmpty = ({ className = '', children, ...props }: ComponentProps) => ( {children ?? 'No attachments'} );