import { useState, useRef } from "react"; import { Send, Paperclip, X, FileText, Image, File } from "lucide-react"; import { Button } from "@/routes/ui/button"; import { cn } from "~/lib/utils"; export interface AttachedFile { /** File ID */ id: string; /** File name */ name: string; /** File type */ type: string; /** File size in bytes */ size: number; /** File object */ file: File; } interface ChatInputProps { /** Callback when message is sent */ onSend: (content: string, attachments?: AttachedFile[]) => void; /** Whether input is disabled */ disabled?: boolean; /** Placeholder text */ placeholder?: string; /** Whether to show attachment button */ showAttachment?: boolean; /** Accepted file types */ acceptedFileTypes?: string; /** Max file size in bytes (default 10MB) */ maxFileSize?: number; /** Quick action suggestions */ suggestions?: string[]; /** Callback when suggestion is clicked */ onSuggestionClick?: (suggestion: string) => void; } /** * Enhanced chat input with attachment support and suggestions * * Features: * - Text input with send button * - File attachment button * - Attached files preview * - Quick action suggestions * - Keyboard shortcuts (Enter to send) */ export function ChatInput({ onSend, disabled, placeholder, showAttachment = true, acceptedFileTypes = ".pdf,.txt,.md,.json,.csv,image/*", maxFileSize = 10 * 1024 * 1024, // 10MB suggestions = [], onSuggestionClick, }: ChatInputProps) { const [input, setInput] = useState(""); const [attachments, setAttachments] = useState([]); const [dragOver, setDragOver] = useState(false); const fileInputRef = useRef(null); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if ((input.trim() || attachments.length > 0) && !disabled) { onSend(input.trim(), attachments.length > 0 ? attachments : undefined); setInput(""); setAttachments([]); } }; const handleFileSelect = (files: FileList | null) => { if (!files) return; const newAttachments: AttachedFile[] = []; Array.from(files).forEach(file => { // Check file size if (file.size > maxFileSize) { console.warn(`File ${file.name} exceeds max size of ${maxFileSize / 1024 / 1024}MB`); return; } newAttachments.push({ id: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, name: file.name, type: file.type, size: file.size, file, }); }); setAttachments(prev => [...prev, ...newAttachments]); }; const handleRemoveAttachment = (id: string) => { setAttachments(prev => prev.filter(a => a.id !== id)); }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setDragOver(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); handleFileSelect(e.dataTransfer.files); }; const getFileIcon = (type: string) => { if (type.startsWith("image/")) return Image; if (type.includes("pdf") || type.includes("text")) return FileText; return File; }; const formatFileSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; return (
{/* Quick suggestions */} {suggestions.length > 0 && input.length === 0 && attachments.length === 0 && (
{suggestions.slice(0, 3).map((suggestion, index) => ( ))}
)} {/* Attached files preview */} {attachments.length > 0 && (
{attachments.map((attachment) => { const FileIcon = getFileIcon(attachment.type); return (
{attachment.name} ({formatFileSize(attachment.size)})
); })}
)} {/* Input form */}
{/* Attachment button */} {showAttachment && ( <> handleFileSelect(e.target.files)} className="hidden" /> )} {/* Text input */}