import React, { useRef, useState, useEffect, useCallback } from "react"; import { Image as ImageIcon, Send, Trash2, ArrowUp } from "lucide-react"; import { MessageBlock } from "./types"; import { useUploadFile } from "@/app/service/domain/service"; import { motion, AnimatePresence } from "framer-motion"; import { QuickActionTags, QuickAction } from "./QuickActionTags"; import { useDeviceType } from '@/hooks/useDeviceType'; // 防抖函数 function debounce void>(func: T, wait: number) { let timeout: NodeJS.Timeout | null = null; return function executedFunction(...args: Parameters) { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { func(...args); timeout = null; }, wait); }; }; interface InputBarProps { onSend: (blocks: MessageBlock[], videoId?: string) => void; setVideoPreview?: (url: string, id: string) => void; initialVideoUrl?: string; initialVideoId?: string; setIsFocusChatInput?: (v: boolean) => void; } export function InputBar({ onSend, setVideoPreview, initialVideoUrl, initialVideoId, setIsFocusChatInput }: InputBarProps) { const [text, setText] = useState(""); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [imageUrl, setImageUrl] = useState(null); const [videoUrl, setVideoUrl] = useState(initialVideoUrl || null); const [videoId, setVideoId] = useState(initialVideoId || null); const [isMultiline, setIsMultiline] = useState(false); const { isMobile, isTablet, isDesktop } = useDeviceType(); const textareaRef = useRef(null); const { uploadFile } = useUploadFile(); const [isComposing, setIsComposing] = useState(false); const adjustHeight = useCallback(() => { const textarea = textareaRef.current; if (!textarea || isComposing) return; // 保存当前滚动位置 const scrollPos = window.scrollY; // 重置高度以获取实际内容高度 textarea.style.height = '0px'; // 强制浏览器重排,获取准确的 scrollHeight const scrollHeight = textarea.scrollHeight; const newHeight = Math.min(Math.max(scrollHeight, 48), 120); // 设置新高度 textarea.style.height = `${newHeight}px`; // 恢复滚动位置,避免页面跳动 window.scrollTo(0, scrollPos); // 更新布局状态 if (!text.trim()) { setIsMultiline(false); } else if (!isMultiline && newHeight > 48) { setIsMultiline(true); } }, [text, isMultiline, isComposing]); // 使用防抖包装 adjustHeight,但确保在输入完成时立即调整 const debouncedAdjustHeight = useCallback( debounce(() => { requestAnimationFrame(() => { if (!isComposing) { adjustHeight(); } }); }, 16), // 使用一帧的时间作为防抖时间 [adjustHeight, isComposing] ); // 监听初始视频 URL 和 ID 的变化 useEffect(() => { if (initialVideoUrl && initialVideoId) { setVideoUrl(initialVideoUrl); setVideoId(initialVideoId); } }, [initialVideoUrl, initialVideoId]); // 监听文本变化和组件挂载时调整高度 useEffect(() => { debouncedAdjustHeight(); }, [text, debouncedAdjustHeight]); // 布局切换时保持输入框焦点并将光标移到末尾 useEffect(() => { // 等待布局动画完成后再聚焦,避免动画过程中的视觉跳动 const focusTimeout = setTimeout(() => { const textarea = textareaRef.current; if (textarea) { textarea.focus(); // 将光标移动到文本末尾 const length = textarea.value.length; textarea.setSelectionRange(length, length); } }, 200); // 与布局动画时长保持一致 return () => clearTimeout(focusTimeout); }, [isMultiline]); const handleSend = () => { const blocks: MessageBlock[] = []; if (text.trim()) blocks.push({ type: "text" as const, text: text.trim() }); if (imageUrl) blocks.push({ type: "image" as const, url: imageUrl }); if (videoUrl) blocks.push({ type: "video" as const, url: videoUrl }); if (!blocks.length) return; onSend(blocks, videoId || undefined); setText(""); setImageUrl(null); if (videoUrl && videoId && setVideoPreview) { setVideoPreview(videoUrl, videoId); } setVideoUrl(null); setVideoId(null); }; const handleFileUpload = async (file: File) => { try { setIsUploading(true); const url = await uploadFile(file, (progress) => { setUploadProgress(progress); }); // 如果已经有视频,先保存视频状态 const prevVideoUrl = videoUrl; const prevVideoId = videoId; setImageUrl(url); // 恢复视频状态 if (prevVideoUrl && prevVideoId) { setVideoUrl(prevVideoUrl); setVideoId(prevVideoId); } } catch (error) { console.error("上传失败:", error); // 可以添加错误提示 } finally { setIsUploading(false); setUploadProgress(0); } }; const onFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; // 保存对 input 元素的引用 const inputElement = e.currentTarget; if (file) { if (!file.type.startsWith('image/')) { alert('请选择图片文件'); inputElement.value = ""; // 重置 input return; } await handleFileUpload(file); // 使用保存的引用重置 input inputElement.value = ""; } }; const removeImage = () => { setImageUrl(null); }; return (
{/* 媒体预览 */}
{/* 图片预览 */} {imageUrl && (
预览图
)} {/* 视频预览 */} {videoUrl && (
)}
{/* 上传进度 */} {isUploading && (
)} {/* 快捷操作标签组 */} { // 将标签文本添加到输入框 setText(action.label); // 聚焦输入框并触发高度调整 if (textareaRef.current) { textareaRef.current.focus(); adjustHeight(); } }} /> {/* 图片上传按钮 - 单行时显示在左侧 */} {!isMultiline && ( )} {/* 文本输入 */}