"use client" import { Download, History, Image as ImageIcon, Loader2, Send, Trash2, } from "lucide-react" import type React from "react" import { useCallback, useEffect, useRef, useState } from "react" import { toast } from "sonner" import { ButtonWithTooltip } from "@/components/button-with-tooltip" import { ErrorToast } from "@/components/error-toast" import { HistoryDialog } from "@/components/history-dialog" import { ResetWarningModal } from "@/components/reset-warning-modal" import { SaveDialog } from "@/components/save-dialog" import { Button } from "@/components/ui/button" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { useDiagram } from "@/contexts/diagram-context" import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { FilePreviewList } from "./file-preview-list" const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB const MAX_FILES = 5 function isValidFileType(file: File): boolean { return file.type.startsWith("image/") || isPdfFile(file) || isTextFile(file) } function formatFileSize(bytes: number): string { const mb = bytes / 1024 / 1024 if (mb < 0.01) return `${(bytes / 1024).toFixed(0)}KB` return `${mb.toFixed(2)}MB` } function showErrorToast(message: React.ReactNode) { toast.custom( (t) => ( toast.dismiss(t)} /> ), { duration: 5000 }, ) } interface ValidationResult { validFiles: File[] errors: string[] } function validateFiles( newFiles: File[], existingCount: number, ): ValidationResult { const errors: string[] = [] const validFiles: File[] = [] const availableSlots = MAX_FILES - existingCount if (availableSlots <= 0) { errors.push(`Maximum ${MAX_FILES} files allowed`) return { validFiles, errors } } for (const file of newFiles) { if (validFiles.length >= availableSlots) { errors.push(`Only ${availableSlots} more file(s) allowed`) break } if (!isValidFileType(file)) { errors.push(`"${file.name}" is not a supported file type`) continue } // Only check size for images (PDFs/text files are extracted client-side, so file size doesn't matter) const isExtractedFile = isPdfFile(file) || isTextFile(file) if (!isExtractedFile && file.size > MAX_IMAGE_SIZE) { const maxSizeMB = MAX_IMAGE_SIZE / 1024 / 1024 errors.push( `"${file.name}" is ${formatFileSize(file.size)} (exceeds ${maxSizeMB}MB)`, ) } else { validFiles.push(file) } } return { validFiles, errors } } function showValidationErrors(errors: string[]) { if (errors.length === 0) return if (errors.length === 1) { showErrorToast( {errors[0]}, ) } else { showErrorToast(
{errors.length} files rejected:
, ) } } interface ChatInputProps { input: string status: "submitted" | "streaming" | "ready" | "error" onSubmit: (e: React.FormEvent) => void onChange: (e: React.ChangeEvent) => void onClearChat: () => void files?: File[] onFileChange?: (files: File[]) => void pdfData?: Map< File, { text: string; charCount: number; isExtracting: boolean } > showHistory?: boolean onToggleHistory?: (show: boolean) => void sessionId?: string error?: Error | null minimalStyle?: boolean onMinimalStyleChange?: (value: boolean) => void } export function ChatInput({ input, status, onSubmit, onChange, onClearChat, files = [], onFileChange = () => {}, pdfData = new Map(), showHistory = false, onToggleHistory = () => {}, sessionId, error = null, minimalStyle = false, onMinimalStyleChange = () => {}, }: ChatInputProps) { const { diagramHistory, saveDiagramToFile } = useDiagram() const textareaRef = useRef(null) const fileInputRef = useRef(null) const [isDragging, setIsDragging] = useState(false) const [showClearDialog, setShowClearDialog] = useState(false) const [showSaveDialog, setShowSaveDialog] = useState(false) // Allow retry when there's an error (even if status is still "streaming" or "submitted") const isDisabled = (status === "streaming" || status === "submitted") && !error const adjustTextareaHeight = useCallback(() => { const textarea = textareaRef.current if (textarea) { textarea.style.height = "auto" textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px` } }, []) // Handle programmatic input changes (e.g., setInput("") after form submission) useEffect(() => { adjustTextareaHeight() }, [input, adjustTextareaHeight]) const handleChange = (e: React.ChangeEvent) => { onChange(e) adjustTextareaHeight() } const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault() const form = e.currentTarget.closest("form") if (form && input.trim() && !isDisabled) { form.requestSubmit() } } } const handlePaste = async (e: React.ClipboardEvent) => { if (isDisabled) return const items = e.clipboardData.items const imageItems = Array.from(items).filter((item) => item.type.startsWith("image/"), ) if (imageItems.length > 0) { const imageFiles = ( await Promise.all( imageItems.map(async (item, index) => { const file = item.getAsFile() if (!file) return null return new File( [file], `pasted-image-${Date.now()}-${index}.${file.type.split("/")[1]}`, { type: file.type }, ) }), ) ).filter((f): f is File => f !== null) const { validFiles, errors } = validateFiles( imageFiles, files.length, ) showValidationErrors(errors) if (validFiles.length > 0) { onFileChange([...files, ...validFiles]) } } } const handleFileChange = (e: React.ChangeEvent) => { const newFiles = Array.from(e.target.files || []) const { validFiles, errors } = validateFiles(newFiles, files.length) showValidationErrors(errors) if (validFiles.length > 0) { onFileChange([...files, ...validFiles]) } // Reset input so same file can be selected again if (fileInputRef.current) { fileInputRef.current.value = "" } } const handleRemoveFile = (fileToRemove: File) => { onFileChange(files.filter((file) => file !== fileToRemove)) if (fileInputRef.current) { fileInputRef.current.value = "" } } const triggerFileInput = () => { fileInputRef.current?.click() } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(false) } const handleDrop = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(false) if (isDisabled) return const droppedFiles = e.dataTransfer.files const supportedFiles = Array.from(droppedFiles).filter((file) => isValidFileType(file), ) const { validFiles, errors } = validateFiles( supportedFiles, files.length, ) showValidationErrors(errors) if (validFiles.length > 0) { onFileChange([...files, ...validFiles]) } } const handleClear = () => { onClearChat() setShowClearDialog(false) } return (
{/* File previews */} {files.length > 0 && (
)} {/* Input container */}