import React, { useState, useCallback, useRef, type KeyboardEvent } from 'react' import { ArrowUp, StopCircle, Paperclip, Wrench } from 'lucide-react' import { cn } from '@/utils/cn' import { Textarea } from '@/components/ui/textarea' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import type { SendMessagePayload } from '../types' import { Button } from '@/components/ui' import { useNovaKit } from '../context/useNovaKit' import { useFileUploader } from '../hooks/useFileUploader' import type { UploadFile } from '../types' import { FilePreviewList } from './FilePreviewList' import { request } from '@/http/request' import { McpStorePopover, SkillForm, type SkillFormState, MCPJsonEditor, } from '../tools' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { toast } from 'sonner' import { TaskStatus } from '../types' import { getProjectId, getUserId } from '@/utils/getAuth' interface InputToolsProps { showUpload?: boolean fileInputRef: React.RefObject accept?: string onFileSelect: (e: React.ChangeEvent) => void onOpenSkillManager: () => void onOpenMcpManager: () => void } const InputTools: React.FC = ({ showUpload, fileInputRef, accept, onFileSelect, onOpenSkillManager, onOpenMcpManager, }) => { if (!showUpload) return null return (
{/* 上传文件 */}

上传文件

{/* 中间按钮:Popover,提供 Skill / MCP 选项 */} 添加工具 {/* MCP 市场入口 */}
) } export interface MessageInputProps { /** 占位符文本 */ placeholder?: string /** 是否禁用 */ disabled?: boolean /** 是否正在加载 */ loading?: boolean /** 任务状态 */ taskStatus?: TaskStatus /** 发送消息回调 */ onSend?: (payload: SendMessagePayload) => void /** 终止消息回调 */ onStop?: () => void /** 文件列表变化回调 */ onFilesChange?: (files: UploadFile[]) => void /** 自定义类名 */ className?: string /** 是否显示文件上传按钮 */ showUpload?: boolean } /** * 消息输入框组件 */ export function MessageInput({ placeholder = '请输入消息...', taskStatus = TaskStatus.PENDING, onSend, className, showUpload = true, }: MessageInputProps) { const { api: { stopChat }, loading, agentId } = useNovaKit() const [content, setContent] = useState('') const [files, setFiles] = useState([]) const textareaRef = useRef(null) const fileInputRef = useRef(null) const [skillDialogOpen, setSkillDialogOpen] = useState(false) const [mcpDialogOpen, setMcpDialogOpen] = useState(false) const [mcpJson, setMcpJson] = useState('') const handleUploadEnd = useCallback((file: UploadFile) => { setFiles(prev => prev.map(f => f.uid === file.uid ? file : f)) }, []) const handleUploadStart = useCallback((file: UploadFile) => { setFiles(prev => [...prev, file]) }, []) const handleFileUpdate = useCallback((file: UploadFile) => { setFiles(prev => prev.map(f => f.uid === file.uid ? file : f)) }, []) const { uploadFile, accept } = useFileUploader({ onUploadStart: handleUploadStart, onFileUpdate: handleFileUpdate, onUploadEnd: handleUploadEnd, }) // Filter valid files and check loading status const uploading = files.some(f => f.uploadStatus === 'uploading' || f.uploadStatus === 'pending') const contentEmpty = !content.trim() && files.length === 0 const showStopButton = (loading || taskStatus === TaskStatus.IN_PROGRESS) && taskStatus !== TaskStatus.PAUSED const handleFileSelect = useCallback(async (e: React.ChangeEvent) => { if (e.target.files) { const selectedFiles = Array.from(e.target.files) for (const file of selectedFiles) { await uploadFile(file) } // Reset input if (fileInputRef.current) fileInputRef.current.value = '' } }, [uploadFile]) const removeFile = useCallback((uid: string) => { setFiles(prev => prev.filter(f => f.uid !== uid)) }, []) // 发送消息 const handleSend = useCallback(() => { if (contentEmpty) return // Check if any files are still uploading if (uploading) { console.warn('请等待文件上传完成') return } // Filter out failed uploads const validFiles = files.filter(f => f.uploadStatus === 'success') const fileIds = validFiles.map(f => f.upload_file_id).filter(Boolean) as string[] const payload: SendMessagePayload = { content: content.trim(), upload_file_ids: fileIds.length > 0 ? fileIds : undefined } // Ensure we are sending the file IDs as requested console.log('Sending message payload:', payload) onSend?.(payload) setContent('') setFiles([]) // 清空已上传的文件 }, [content, contentEmpty, onSend, files, uploading]) // 处理键盘事件 const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSend() } }, [handleSend] ) // 自动调整高度 const handleInput = useCallback((e: React.ChangeEvent) => { const textarea = e.target setContent(textarea.value) // 自动调整高度 textarea.style.height = 'auto' textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px` }, []) return ( <>
{/* 渐变光晕背景层 */} {/* 主输入容器:上方 textarea,下方工具栏 / 发送按钮 */}
{/* 文件预览区域 */} {/* 文本输入区域(顶部整行) */}