"use client"; import React, { useCallback, useRef, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { ResetWarningModal } from "@/components/reset-warning-modal"; import { Loader2, Send, RotateCcw, Image as ImageIcon, History, } from "lucide-react"; import { ButtonWithTooltip } from "@/components/button-with-tooltip"; import { FilePreviewList } from "./file-preview-list"; import { useDiagram } from "@/contexts/diagram-context"; import { HistoryDialog } from "@/components/history-dialog"; 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; showHistory?: boolean; onToggleHistory?: (show: boolean) => void; } export function ChatInput({ input, status, onSubmit, onChange, onClearChat, files = [], onFileChange = () => {}, showHistory = false, onToggleHistory = () => {}, }: ChatInputProps) { const { diagramHistory } = useDiagram(); const textareaRef = useRef(null); const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [showClearDialog, setShowClearDialog] = useState(false); // Auto-resize textarea based on content const adjustTextareaHeight = useCallback(() => { const textarea = textareaRef.current; if (textarea) { textarea.style.height = "auto"; textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; } }, []); useEffect(() => { adjustTextareaHeight(); }, [input, adjustTextareaHeight]); // Handle keyboard shortcuts and paste events 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() && status !== "streaming") { form.requestSubmit(); } } }; // Handle clipboard paste const handlePaste = async (e: React.ClipboardEvent) => { if (status === "streaming") 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) => { const file = item.getAsFile(); if (!file) return null; // Create a new file with a unique name return new File( [file], `pasted-image-${Date.now()}.${file.type.split("/")[1]}`, { type: file.type, } ); }) ); const validFiles = imageFiles.filter( (file): file is File => file !== null ); if (validFiles.length > 0) { onFileChange([...files, ...validFiles]); } } }; // Handle file changes const handleFileChange = (e: React.ChangeEvent) => { const newFiles = Array.from(e.target.files || []); onFileChange([...files, ...newFiles]); }; // Remove individual file const handleRemoveFile = (fileToRemove: File) => { onFileChange(files.filter((file) => file !== fileToRemove)); if (fileInputRef.current) { fileInputRef.current.value = ""; } }; // Trigger file input click const triggerFileInput = () => { fileInputRef.current?.click(); }; // Handle drag events 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 (status === "streaming") return; const droppedFiles = e.dataTransfer.files; // Only process image files const imageFiles = Array.from(droppedFiles).filter((file) => file.type.startsWith("image/") ); if (imageFiles.length > 0) { onFileChange([...files, ...imageFiles]); } }; // Handle clearing conversation and diagram const handleClear = () => { onClearChat(); setShowClearDialog(false); }; return (