'use client' import { toast } from 'sonner' import copy from 'copy-to-clipboard' import { Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton, } from '@/components/ai-elements/conversation' import { Message, MessageContent, MessageResponse, MessageAttachments, MessageAttachment } from '@/components/ai-elements/message' import { Actions, Action } from '@/components/ai-elements/actions' import { PromptInput, PromptInputTextarea, PromptInputToolbar, PromptInputTools, PromptInputSubmit, PromptInputAttachments, PromptInputAttachment, PromptInputActionMenu, PromptInputActionMenuTrigger, PromptInputActionMenuContent, PromptInputActionAddAttachments, PromptInputButton, } from '@/components/ai-elements/prompt-input' import { ModelSelector, ModelSelectorContent, ModelSelectorEmpty, ModelSelectorGroup, ModelSelectorInput, ModelSelectorItem, ModelSelectorList, ModelSelectorLogo, ModelSelectorName, ModelSelectorTrigger, } from '@/components/ai-elements/model-selector' import { MessageSquareIcon, BotIcon, CopyIcon, RefreshCcwIcon, CheckIcon, WrenchIcon } from 'lucide-react' import { cn } from '@/lib/utils' import { useChat } from '@ai-sdk/react' import { DefaultChatTransport } from 'ai' import { useState, useEffect } from 'react' import { AGENT_TYPES, AVAILABLE_MODELS, getAgentTypeById, getModelById } from './agents-config' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command' import { DialogDescription } from '@/components/ui/dialog' import { VisuallyHidden } from '@radix-ui/react-visually-hidden' import { Reasoning, ReasoningContent, ReasoningTrigger } from '@/components/ai-elements/reasoning' import { ScrollArea } from '@/components/ui/scroll-area' export function DevAIChat() { // 智能体、模型和工具的状态管理 const [selectedAgent, setSelectedAgent] = useState(AGENT_TYPES[0].id) const [selectedModel, setSelectedModel] = useState(AGENT_TYPES[0].defaultModel) const [selectedTools, setSelectedTools] = useState(AGENT_TYPES[0].defaultTools) const [agentSelectorOpen, setAgentSelectorOpen] = useState(false) const [modelSelectorOpen, setModelSelectorOpen] = useState(false) const [toolSelectorOpen, setToolSelectorOpen] = useState(false) const { messages, status, sendMessage, regenerate } = useChat({ transport: new DefaultChatTransport({ api: '/api/dev/ai-chat', }), }) // 获取当前选中的智能体配置 const currentAgent = getAgentTypeById(selectedAgent) const currentModel = getModelById(selectedModel) // 当智能体切换时,自动选中默认的模型和工具 useEffect(() => { const agent = getAgentTypeById(selectedAgent) if (agent) { setSelectedModel(agent.defaultModel) setSelectedTools(agent.defaultTools) } }, [selectedAgent]) // 切换工具选择 const toggleTool = (toolId: string) => { setSelectedTools(prev => prev.includes(toolId) ? prev.filter(id => id !== toolId) : [...prev, toolId] ) } // 复制文本到剪贴板 const handleCopy = (text: string) => { const success = copy(text) if (success) { toast.success('已复制到剪贴板') } else { toast.error('复制失败') } } // 重新生成回复 const handleRegenerate = () => { regenerate({ body: { agent: selectedAgent, model: selectedModel, tools: selectedTools, }, }) } return (
{/* 消息列表区域 - 占据剩余空间 */} {messages.length === 0 ? ( } /> ) : ( messages.map((message, messageIndex) => { const isLastMessage = messageIndex === messages.length - 1 const isAssistant = message.role === 'assistant' const messageText = message.parts .filter(part => part.type === 'text') .map(part => part.type === 'text' ? part.text : '') .join('') return (
{/* 先渲染附件 */} {message.parts.some(part => part.type === 'file') && ( {message.parts .filter(part => part.type === 'file') .map((part, i) => ( ))} )} {/* 然后渲染其他内容 */} {message.parts.map((part, i) => { switch (part.type) { case 'text': return ( {part.text} ); case 'reasoning': return ( {part.text} ); case 'file': // 文件已经在上面单独渲染了 return null; default: return null; } })} {/* 操作按钮 - 悬停时显示 */} {/* 复制按钮 - 所有消息都有 */} handleCopy(messageText)} > {/* 重新生成按钮 - 仅最后一条AI消息显示 */} {isAssistant && isLastMessage && ( )}
) }) )}
{/* 输入框区域 - 固定在底部 */}
{ if ('message' in error) { console.error('文件上传错误:', error.message) // 根据错误类型显示不同的提示 switch (error.code) { case 'max_file_size': toast.error('文件过大', { description: '单个文件大小不能超过 10MB,请压缩后重试' }) break case 'max_files': toast.error('文件数量超限', { description: '最多只能上传 5 个文件' }) break case 'accept': toast.error('文件类型不支持', { description: '仅支持图片文件' }) break default: toast.error('文件上传失败', { description: error.message }) } } }} onSubmit={async (message) => { sendMessage( { text: message.text || '', files: message.files, }, { body: { agent: selectedAgent, model: selectedModel, tools: selectedTools, }, } ) }} className="max-h-[50vh]" > {/* 附件预览区域 */} {(attachment) => } {/* 文本输入框 */} {/* 工具栏 */} {/* 智能体选择器 */} {currentAgent?.name} 未找到智能体 {AGENT_TYPES.map((agent) => ( { setSelectedAgent(agent.id) setAgentSelectorOpen(false) }} >
{agent.name} {agent.description}
{selectedAgent === agent.id && ( )}
))}
{/* 模型选择器 */} {currentModel?.name} 选择模型 未找到模型 {AVAILABLE_MODELS.map((model) => ( { setSelectedModel(model.id) setModelSelectorOpen(false) }} > {model.name} {selectedModel === model.id && ( )} ))} {/* 工具选择器 */} {currentAgent && currentAgent.availableTools.length > 0 && ( {selectedTools.length} 个工具 未找到工具 {currentAgent.availableTools.map((tool) => ( toggleTool(tool.id)} >
{tool.name} {tool.description}
{selectedTools.includes(tool.id) && ( )}
))}
)}
{/* 右侧按钮组 */}
{/* 附件上传菜单 */} {/* 发送按钮 */}
) }