import { useState, useRef, useEffect, useCallback } from 'react' import { Loader2, X, RotateCcw, ImagePlus } from 'lucide-react' import { cn } from '@/lib/utils' import { uploadsApi } from '@/api/uploads' import type { FileUploadResponse, PendingUpload } from '@/types/upload' interface RichTextInputProps { value: string onChange: (value: string) => void onFilesChange?: (uploads: FileUploadResponse[]) => void sessionId?: string placeholder?: string rows?: number className?: string disabled?: boolean } export function RichTextInput({ value, onChange, onFilesChange, sessionId, placeholder, rows = 3, className, disabled, }: RichTextInputProps) { const [pendingUploads, setPendingUploads] = useState([]) const [isDragOver, setIsDragOver] = useState(false) const [isFocused, setIsFocused] = useState(false) const textareaRef = useRef(null) const dragCounterRef = useRef(0) // Cleanup blob URLs on unmount useEffect(() => { return () => { pendingUploads.forEach((upload) => { URL.revokeObjectURL(upload.preview) }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const processFiles = useCallback( (files: File[]) => { const imageFiles = files.filter((f) => f.type.startsWith('image/')) if (imageFiles.length === 0) return const newUploads: PendingUpload[] = imageFiles.map((file) => ({ id: crypto.randomUUID(), file, preview: URL.createObjectURL(file), status: 'uploading' as const, })) setPendingUploads((prev) => [...prev, ...newUploads]) // Upload each file newUploads.forEach((upload) => { uploadsApi .upload(upload.file, sessionId) .then((result) => { setPendingUploads((prev) => { const updated = prev.map((u) => u.id === upload.id ? { ...u, status: 'done' as const, result } : u ) // Notify parent of all completed uploads const completed = updated .filter((u) => u.status === 'done' && u.result) .map((u) => u.result!) onFilesChange?.(completed) return updated }) }) .catch((err) => { const errorMsg = err?.response?.status === 503 ? 'File uploads not available — contact your administrator' : err?.message || 'Upload failed' setPendingUploads((prev) => prev.map((u) => u.id === upload.id ? { ...u, status: 'error' as const, error: errorMsg } : u ) ) }) }) }, [sessionId, onFilesChange] ) const handlePaste = useCallback( (e: React.ClipboardEvent) => { if (disabled) return const items = e.clipboardData?.items if (!items) return const imageFiles: File[] = [] for (let i = 0; i < items.length; i++) { const item = items[i] if (item.type.startsWith('image/')) { const file = item.getAsFile() if (file) imageFiles.push(file) } } if (imageFiles.length > 0) { e.preventDefault() processFiles(imageFiles) } }, [disabled, processFiles] ) const handleDragOver = useCallback( (e: React.DragEvent) => { if (disabled) return e.preventDefault() e.dataTransfer.dropEffect = 'copy' }, [disabled] ) const handleDragEnter = useCallback( (e: React.DragEvent) => { if (disabled) return e.preventDefault() dragCounterRef.current++ if (dragCounterRef.current === 1) { setIsDragOver(true) } }, [disabled] ) const handleDragLeave = useCallback( (e: React.DragEvent) => { e.preventDefault() dragCounterRef.current-- if (dragCounterRef.current === 0) { setIsDragOver(false) } }, [] ) const handleDrop = useCallback( (e: React.DragEvent) => { if (disabled) return e.preventDefault() dragCounterRef.current = 0 setIsDragOver(false) const files = Array.from(e.dataTransfer.files) processFiles(files) }, [disabled, processFiles] ) const handleRemove = useCallback( (uploadId: string) => { setPendingUploads((prev) => { const toRemove = prev.find((u) => u.id === uploadId) if (toRemove) { URL.revokeObjectURL(toRemove.preview) } const updated = prev.filter((u) => u.id !== uploadId) const completed = updated .filter((u) => u.status === 'done' && u.result) .map((u) => u.result!) onFilesChange?.(completed) return updated }) }, [onFilesChange] ) const retryUpload = useCallback( (uploadId: string) => { setPendingUploads((prev) => prev.map((u) => (u.id === uploadId ? { ...u, status: 'uploading' as const, error: undefined } : u)) ) const upload = pendingUploads.find((u) => u.id === uploadId) if (!upload) return uploadsApi .upload(upload.file, sessionId) .then((result) => { setPendingUploads((prev) => { const updated = prev.map((u) => u.id === uploadId ? { ...u, status: 'done' as const, result } : u ) const completed = updated .filter((u) => u.status === 'done' && u.result) .map((u) => u.result!) onFilesChange?.(completed) return updated }) }) .catch((err) => { const errorMsg = err?.response?.status === 503 ? 'File uploads not available — contact your administrator' : err?.message || 'Upload failed' setPendingUploads((prev) => prev.map((u) => u.id === uploadId ? { ...u, status: 'error' as const, error: errorMsg } : u ) ) }) }, [pendingUploads, sessionId, onFilesChange] ) return (