"use client"; import { FormEvent, useEffect, useRef, useState, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { uploadFile, UploadFileResult } from "@/features/agent/services/messageApi"; import { X, Paperclip, Image as ImageIcon } from "lucide-react"; import { toast } from "@/hooks/useToast"; import { useI18n } from "@/lib/i18n/provider"; interface MessageInputProps { value: string; onChange: (value: string) => void; onSubmit: (fileInfo?: UploadFileResult) => Promise | void; sending: boolean; conversationId?: number; // 对话ID,用于文件上传 } interface FilePreview { file: File; preview?: string; // 图片预览URL } export function MessageInput({ value, onChange, onSubmit, sending, conversationId, }: MessageInputProps) { const { t } = useI18n(); // 输入框引用,用于发送消息后自动聚焦 const inputRef = useRef(null); // 文件输入框引用 const fileInputRef = useRef(null); // 记录上一次的 sending 状态,用于判断是否刚刚完成发送 const prevSendingRef = useRef(false); // 文件预览状态 const [filePreview, setFilePreview] = useState(null); // 上传中状态 const [uploading, setUploading] = useState(false); // 当发送状态从 true 变为 false 时(发送完成),自动聚焦到输入框 useEffect(() => { // 如果上一次是发送中(true),现在是发送完成(false),说明刚刚发送完成 if (prevSendingRef.current && !sending && inputRef.current) { // 使用 setTimeout 确保 DOM 更新完成后再聚焦 // 这样可以避免在某些情况下聚焦失败 setTimeout(() => { inputRef.current?.focus(); }, 0); } // 更新上一次的 sending 状态 prevSendingRef.current = sending; }, [sending]); // 处理文件选择 const handleFileSelect = useCallback( async (file: File) => { // 验证文件大小(10MB) const MAX_FILE_SIZE = 10 * 1024 * 1024; if (file.size > MAX_FILE_SIZE) { toast.error(t("agent.input.fileTooLarge")); return; } // 验证文件类型 const ext = file.name.toLowerCase().split(".").pop(); const allowedExts = ["jpg", "jpeg", "png", "gif", "webp", "pdf", "doc", "docx", "txt"]; if (!ext || !allowedExts.includes(ext)) { toast.error(t("agent.input.fileTypeNotSupported")); return; } // 如果是图片,生成预览 let preview: string | undefined; if (file.type.startsWith("image/")) { preview = URL.createObjectURL(file); } setFilePreview({ file, preview }); }, [] ); // 处理文件输入框变化 const handleFileInputChange = useCallback( (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { handleFileSelect(file); } // 清空文件输入框,允许重复选择同一文件 if (fileInputRef.current) { fileInputRef.current.value = ""; } }, [handleFileSelect] ); // 处理拖拽上传 const handleDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); event.stopPropagation(); }, []); const handleDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); event.stopPropagation(); const file = event.dataTransfer.files?.[0]; if (file) { handleFileSelect(file); } }, [handleFileSelect] ); // 处理粘贴图片 useEffect(() => { const handlePaste = (event: ClipboardEvent) => { const items = event.clipboardData?.items; if (!items) return; for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.type.startsWith("image/")) { const file = item.getAsFile(); if (file) { event.preventDefault(); handleFileSelect(file); break; } } } }; const input = inputRef.current; if (input) { input.addEventListener("paste", handlePaste); return () => { input.removeEventListener("paste", handlePaste); }; } }, [handleFileSelect]); // 移除文件预览 const handleRemoveFile = useCallback(() => { if (filePreview?.preview) { URL.revokeObjectURL(filePreview.preview); } setFilePreview(null); }, [filePreview]); // 格式化文件大小 const formatFileSize = (bytes: number): string => { if (bytes < 1024) return bytes + " B"; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; return (bytes / (1024 * 1024)).toFixed(1) + " MB"; }; // 处理提交 const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (sending || uploading) { return; } // 验证:必须有内容或文件 if (!value.trim() && !filePreview) { return; } try { let fileInfo: UploadFileResult | undefined; // 如果有文件,先上传文件 if (filePreview) { setUploading(true); try { fileInfo = await uploadFile(filePreview.file, conversationId); } catch (error) { toast.error((error as Error).message || t("agent.input.uploadFailed")); setUploading(false); return; } setUploading(false); } // 发送消息(包含文件信息) await onSubmit(fileInfo); // 清空输入和文件预览 onChange(""); handleRemoveFile(); } catch (error) { console.error("发送消息失败:", error); } }; // 清理预览URL useEffect(() => { return () => { if (filePreview?.preview) { URL.revokeObjectURL(filePreview.preview); } }; }, [filePreview]); return (
{/* 文件预览区域 */} {filePreview && (
{filePreview.preview ? ( // 图片预览
预览
{filePreview.file.name} ({formatFileSize(filePreview.file.size)})
) : ( // 文档预览
{filePreview.file.name}
{formatFileSize(filePreview.file.size)}
)}
)} {/* 输入区域 */}
onChange(event.target.value)} className="flex-1 border-border/50 focus:border-primary/50 focus:ring-primary/20" disabled={sending || uploading} />
); }