diff --git a/src/views/AgentChat/hooks/index.ts b/src/views/AgentChat/hooks/index.ts new file mode 100644 index 00000000..bb28170e --- /dev/null +++ b/src/views/AgentChat/hooks/index.ts @@ -0,0 +1,34 @@ +// src/views/AgentChat/hooks/index.ts +// 自定义 Hooks 统一导出 + +/** + * 自定义 Hooks 统一入口 + * + * 使用示例: + * ```typescript + * // 方式 1: 从统一入口导入(推荐) + * import { useAgentChat, useAgentSessions, useFileUpload, useAutoScroll } from './hooks'; + * + * // 方式 2: 从单个文件导入 + * import { useAgentChat } from './hooks/useAgentChat'; + * ``` + */ + +export { useAutoScroll } from './useAutoScroll'; +export { useFileUpload } from './useFileUpload'; +export type { UploadedFile, UseFileUploadReturn } from './useFileUpload'; + +export { useAgentSessions } from './useAgentSessions'; +export type { + Session, + User, + UseAgentSessionsParams, + UseAgentSessionsReturn, +} from './useAgentSessions'; + +export { useAgentChat } from './useAgentChat'; +export type { + ToastFunction, + UseAgentChatParams, + UseAgentChatReturn, +} from './useAgentChat'; diff --git a/src/views/AgentChat/hooks/useAgentChat.ts b/src/views/AgentChat/hooks/useAgentChat.ts new file mode 100644 index 00000000..747b3489 --- /dev/null +++ b/src/views/AgentChat/hooks/useAgentChat.ts @@ -0,0 +1,289 @@ +// src/views/AgentChat/hooks/useAgentChat.ts +// 消息处理 Hook - 发送消息、处理响应、错误处理 + +import { useState, useCallback } from 'react'; +import type { Dispatch, SetStateAction, KeyboardEvent } from 'react'; +import axios from 'axios'; +import { logger } from '@utils/logger'; +import { MessageTypes, type Message } from '../constants/messageTypes'; +import type { UploadedFile } from './useFileUpload'; +import type { User } from './useAgentSessions'; + +/** + * Toast 通知函数类型(来自 Chakra UI) + */ +export interface ToastFunction { + (options: { + title: string; + description?: string; + status: 'success' | 'error' | 'warning' | 'info'; + duration?: number; + isClosable?: boolean; + }): void; +} + +/** + * useAgentChat Hook 参数 + */ +export interface UseAgentChatParams { + /** 当前用户信息 */ + user: User | null; + /** 当前会话 ID */ + currentSessionId: string | null; + /** 设置当前会话 ID */ + setCurrentSessionId: Dispatch>; + /** 选中的 AI 模型 */ + selectedModel: string; + /** 选中的工具列表 */ + selectedTools: string[]; + /** 已上传文件列表 */ + uploadedFiles: UploadedFile[]; + /** 清空已上传文件 */ + clearFiles: () => void; + /** Toast 通知函数 */ + toast: ToastFunction; + /** 重新加载会话列表(发送消息成功后调用) */ + loadSessions: () => Promise; +} + +/** + * useAgentChat Hook 返回值 + */ +export interface UseAgentChatReturn { + /** 消息列表 */ + messages: Message[]; + /** 设置消息列表 */ + setMessages: Dispatch>; + /** 输入框内容 */ + inputValue: string; + /** 设置输入框内容 */ + setInputValue: Dispatch>; + /** 是否正在处理消息 */ + isProcessing: boolean; + /** 发送消息 */ + handleSendMessage: () => Promise; + /** 键盘事件处理(Enter 发送) */ + handleKeyPress: (e: KeyboardEvent) => void; + /** 添加消息到列表 */ + addMessage: (message: Partial) => void; +} + +/** + * useAgentChat Hook + * + * 处理消息发送、AI 响应、错误处理逻辑 + * + * @param params - UseAgentChatParams + * @returns UseAgentChatReturn + * + * @example + * ```tsx + * const { + * messages, + * inputValue, + * setInputValue, + * isProcessing, + * handleSendMessage, + * handleKeyPress, + * } = useAgentChat({ + * user, + * currentSessionId, + * setCurrentSessionId, + * selectedModel, + * selectedTools, + * uploadedFiles, + * clearFiles, + * toast, + * loadSessions, + * }); + * ``` + */ +export const useAgentChat = ({ + user, + currentSessionId, + setCurrentSessionId, + selectedModel, + selectedTools, + uploadedFiles, + clearFiles, + toast, + loadSessions, +}: UseAgentChatParams): UseAgentChatReturn => { + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + + /** + * 添加消息到列表 + */ + const addMessage = useCallback((message: Partial) => { + setMessages((prev) => [ + ...prev, + { + id: Date.now() + Math.random(), + timestamp: new Date().toISOString(), + ...message, + } as Message, + ]); + }, []); + + /** + * 发送消息到后端 API + */ + const handleSendMessage = useCallback(async () => { + if (!inputValue.trim() || isProcessing) return; + + // 创建用户消息 + const userMessage: Partial = { + type: MessageTypes.USER, + content: inputValue, + timestamp: new Date().toISOString(), + files: uploadedFiles.length > 0 ? uploadedFiles : undefined, + }; + + addMessage(userMessage); + const userInput = inputValue; + + // 清空输入框和文件 + setInputValue(''); + clearFiles(); + setIsProcessing(true); + + try { + // 显示 "思考中" 状态 + addMessage({ + type: MessageTypes.AGENT_THINKING, + content: '正在分析你的问题...', + }); + + // 调用后端 API + const response = await axios.post('/mcp/agent/chat', { + message: userInput, + conversation_history: messages + .filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) + .map((m) => ({ + isUser: m.type === MessageTypes.USER, + content: m.content, + })), + user_id: user?.id || 'anonymous', + user_nickname: user?.nickname || '匿名用户', + user_avatar: user?.avatar || '', + subscription_type: user?.subscription_type || 'free', + session_id: currentSessionId, + model: selectedModel, + tools: selectedTools, + files: uploadedFiles.length > 0 ? uploadedFiles : undefined, + }); + + // 移除 "思考中" 消息 + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); + + if (response.data.success) { + const data = response.data; + + // 更新会话 ID(如果是新会话) + if (data.session_id && !currentSessionId) { + setCurrentSessionId(data.session_id); + } + + // 显示执行计划(如果有) + if (data.plan) { + addMessage({ + type: MessageTypes.AGENT_PLAN, + content: '已制定执行计划', + plan: data.plan, + }); + } + + // 显示执行步骤(如果有) + if (data.steps && data.steps.length > 0) { + addMessage({ + type: MessageTypes.AGENT_EXECUTING, + content: '正在执行步骤...', + plan: data.plan, + stepResults: data.steps, + }); + } + + // 移除 "执行中" 消息 + setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); + + // 显示最终回复 + addMessage({ + type: MessageTypes.AGENT_RESPONSE, + content: data.final_answer || data.message || '处理完成', + plan: data.plan, + stepResults: data.steps, + metadata: data.metadata, + }); + + // 重新加载会话列表 + loadSessions(); + } + } catch (error: any) { + logger.error('Agent chat error', error); + + // 移除 "思考中" 和 "执行中" 消息 + setMessages((prev) => + prev.filter( + (m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING + ) + ); + + // 显示错误消息 + const errorMessage = error.response?.data?.error || error.message || '处理失败'; + addMessage({ + type: MessageTypes.ERROR, + content: `处理失败:${errorMessage}`, + }); + + // 显示 Toast 通知 + toast({ + title: '处理失败', + description: errorMessage, + status: 'error', + duration: 5000, + }); + } finally { + setIsProcessing(false); + } + }, [ + inputValue, + isProcessing, + uploadedFiles, + messages, + user, + currentSessionId, + selectedModel, + selectedTools, + addMessage, + clearFiles, + setCurrentSessionId, + loadSessions, + toast, + ]); + + /** + * 键盘事件处理(Enter 发送,Shift+Enter 换行) + */ + const handleKeyPress = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }, + [handleSendMessage] + ); + + return { + messages, + setMessages, + inputValue, + setInputValue, + isProcessing, + handleSendMessage, + handleKeyPress, + addMessage, + }; +}; diff --git a/src/views/AgentChat/hooks/useAgentSessions.ts b/src/views/AgentChat/hooks/useAgentSessions.ts new file mode 100644 index 00000000..0228d97e --- /dev/null +++ b/src/views/AgentChat/hooks/useAgentSessions.ts @@ -0,0 +1,189 @@ +// src/views/AgentChat/hooks/useAgentSessions.ts +// 会话管理 Hook - 加载、切换、创建会话 + +import { useState, useEffect, useCallback } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import axios from 'axios'; +import { logger } from '@utils/logger'; +import { MessageTypes, type Message } from '../constants/messageTypes'; + +/** + * 会话数据结构 + */ +export interface Session { + session_id: string; + title?: string; + created_at?: string; + timestamp?: string; + message_count?: number; +} + +/** + * 用户信息(从 AuthContext 传入) + */ +export interface User { + id: string; + nickname?: string; + avatar?: string; + subscription_type?: string; +} + +/** + * useAgentSessions Hook 参数 + */ +export interface UseAgentSessionsParams { + /** 当前用户信息 */ + user: User | null; + /** 消息列表 setter(用于创建新会话时设置欢迎消息) */ + setMessages: Dispatch>; +} + +/** + * useAgentSessions Hook 返回值 + */ +export interface UseAgentSessionsReturn { + /** 会话列表 */ + sessions: Session[]; + /** 当前选中的会话 ID */ + currentSessionId: string | null; + /** 设置当前会话 ID */ + setCurrentSessionId: Dispatch>; + /** 是否正在加载会话列表 */ + isLoadingSessions: boolean; + /** 加载会话列表 */ + loadSessions: () => Promise; + /** 切换到指定会话 */ + switchSession: (sessionId: string) => void; + /** 创建新会话(显示欢迎消息) */ + createNewSession: () => void; + /** 加载指定会话的历史消息 */ + loadSessionHistory: (sessionId: string) => Promise; +} + +/** + * useAgentSessions Hook + * + * 管理会话列表、会话切换、新建会话逻辑 + * + * @param params - UseAgentSessionsParams + * @returns UseAgentSessionsReturn + * + * @example + * ```tsx + * const { + * sessions, + * currentSessionId, + * isLoadingSessions, + * switchSession, + * createNewSession, + * } = useAgentSessions({ user, setMessages }); + * ``` + */ +export const useAgentSessions = ({ + user, + setMessages, +}: UseAgentSessionsParams): UseAgentSessionsReturn => { + const [sessions, setSessions] = useState([]); + const [currentSessionId, setCurrentSessionId] = useState(null); + const [isLoadingSessions, setIsLoadingSessions] = useState(false); + + /** + * 加载用户的会话列表 + */ + const loadSessions = useCallback(async () => { + if (!user?.id) return; + + setIsLoadingSessions(true); + try { + const response = await axios.get('/mcp/agent/sessions', { + params: { user_id: user.id, limit: 50 }, + }); + + if (response.data.success) { + setSessions(response.data.data); + } + } catch (error) { + logger.error('加载会话列表失败', error); + } finally { + setIsLoadingSessions(false); + } + }, [user?.id]); + + /** + * 加载指定会话的历史消息 + */ + const loadSessionHistory = useCallback( + async (sessionId: string) => { + if (!sessionId) return; + + try { + const response = await axios.get(`/mcp/agent/history/${sessionId}`, { + params: { limit: 100 }, + }); + + if (response.data.success) { + const history = response.data.data; + const formattedMessages: Message[] = history.map((msg: any, idx: number) => ({ + id: `${sessionId}-${idx}`, + type: msg.message_type === 'user' ? MessageTypes.USER : MessageTypes.AGENT_RESPONSE, + content: msg.message, + plan: msg.plan ? JSON.parse(msg.plan) : null, + stepResults: msg.steps ? JSON.parse(msg.steps) : null, + timestamp: msg.timestamp, + })); + + setMessages(formattedMessages); + } + } catch (error) { + logger.error('加载会话历史失败', error); + } + }, + [setMessages] + ); + + /** + * 切换到指定会话 + */ + const switchSession = useCallback( + (sessionId: string) => { + setCurrentSessionId(sessionId); + loadSessionHistory(sessionId); + }, + [loadSessionHistory] + ); + + /** + * 创建新会话(清空消息,显示欢迎消息) + */ + const createNewSession = useCallback(() => { + setCurrentSessionId(null); + setMessages([ + { + id: Date.now(), + type: MessageTypes.AGENT_RESPONSE, + content: `你好${user?.nickname || ''}!👋\n\n我是**价小前**,你的 AI 投研助手。\n\n**我能做什么?**\n• 📊 全面分析股票基本面和技术面\n• 🔥 追踪市场热点和涨停板块\n• 📈 研究行业趋势和投资机会\n• 📰 汇总最新财经新闻和研报\n\n直接输入你的问题开始探索!`, + timestamp: new Date().toISOString(), + }, + ]); + }, [user?.nickname, setMessages]); + + /** + * 组件挂载时加载会话列表并创建新会话 + */ + useEffect(() => { + loadSessions(); + createNewSession(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user]); + + return { + sessions, + currentSessionId, + setCurrentSessionId, + isLoadingSessions, + loadSessions, + switchSession, + createNewSession, + loadSessionHistory, + }; +}; diff --git a/src/views/AgentChat/hooks/useAutoScroll.ts b/src/views/AgentChat/hooks/useAutoScroll.ts new file mode 100644 index 00000000..de9eb290 --- /dev/null +++ b/src/views/AgentChat/hooks/useAutoScroll.ts @@ -0,0 +1,38 @@ +// src/views/AgentChat/hooks/useAutoScroll.ts +// 自动滚动 Hook - 消息列表自动滚动到底部 + +import { useEffect, useRef } from 'react'; +import type { Message } from '../constants/messageTypes'; + +/** + * useAutoScroll Hook + * + * 监听消息列表变化,自动滚动到底部 + * + * @param messages - 消息列表 + * @returns messagesEndRef - 消息列表底部引用(需要绑定到消息列表末尾的 div) + * + * @example + * ```tsx + * const { messagesEndRef } = useAutoScroll(messages); + * + * return ( + * + * {messages.map(msg => )} + *
+ * + * ); + * ``` + */ +export const useAutoScroll = (messages: Message[]) => { + const messagesEndRef = useRef(null); + + useEffect(() => { + // 平滑滚动到底部 + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + return { + messagesEndRef, + }; +}; diff --git a/src/views/AgentChat/hooks/useFileUpload.ts b/src/views/AgentChat/hooks/useFileUpload.ts new file mode 100644 index 00000000..5fbcd0dc --- /dev/null +++ b/src/views/AgentChat/hooks/useFileUpload.ts @@ -0,0 +1,131 @@ +// src/views/AgentChat/hooks/useFileUpload.ts +// 文件上传 Hook - 处理文件选择、预览、删除 + +import { useState, useRef } from 'react'; +import type { ChangeEvent, RefObject } from 'react'; + +/** + * 上传文件数据结构 + */ +export interface UploadedFile { + /** 文件名 */ + name: string; + /** 文件大小(字节) */ + size: number; + /** 文件 MIME 类型 */ + type: string; + /** 文件预览 URL(使用 URL.createObjectURL 创建) */ + url: string; +} + +/** + * useFileUpload Hook 返回值 + */ +export interface UseFileUploadReturn { + /** 已上传文件列表 */ + uploadedFiles: UploadedFile[]; + /** 文件输入框引用(用于触发文件选择) */ + fileInputRef: RefObject; + /** 处理文件选择事件 */ + handleFileSelect: (event: ChangeEvent) => void; + /** 删除指定文件 */ + removeFile: (index: number) => void; + /** 清空所有文件 */ + clearFiles: () => void; +} + +/** + * useFileUpload Hook + * + * 处理文件上传相关逻辑(选择、预览、删除) + * + * @returns UseFileUploadReturn + * + * @example + * ```tsx + * const { uploadedFiles, fileInputRef, handleFileSelect, removeFile } = useFileUpload(); + * + * return ( + * <> + * + * + * {uploadedFiles.map((file, idx) => ( + * + * {file.name} + * removeFile(idx)} /> + * + * ))} + * + * ); + * ``` + */ +export const useFileUpload = (): UseFileUploadReturn => { + const [uploadedFiles, setUploadedFiles] = useState([]); + const fileInputRef = useRef(null); + + /** + * 处理文件选择事件 + */ + const handleFileSelect = (event: ChangeEvent) => { + const files = Array.from(event.target.files || []); + + const fileData: UploadedFile[] = files.map((file) => ({ + name: file.name, + size: file.size, + type: file.type, + // 创建本地预览 URL(实际上传时需要转换为 base64 或上传到服务器) + url: URL.createObjectURL(file), + })); + + setUploadedFiles((prev) => [...prev, ...fileData]); + + // 清空 input value,允许重复选择同一文件 + if (event.target) { + event.target.value = ''; + } + }; + + /** + * 删除指定索引的文件 + */ + const removeFile = (index: number) => { + setUploadedFiles((prev) => { + // 释放 URL.createObjectURL 创建的内存 + const file = prev[index]; + if (file?.url) { + URL.revokeObjectURL(file.url); + } + + return prev.filter((_, i) => i !== index); + }); + }; + + /** + * 清空所有文件 + */ + const clearFiles = () => { + // 释放所有 URL 内存 + uploadedFiles.forEach((file) => { + if (file.url) { + URL.revokeObjectURL(file.url); + } + }); + + setUploadedFiles([]); + }; + + return { + uploadedFiles, + fileInputRef, + handleFileSelect, + removeFile, + clearFiles, + }; +}; diff --git a/src/views/AgentChat/index.js b/src/views/AgentChat/index.js index 7db61de8..8531c823 100644 --- a/src/views/AgentChat/index.js +++ b/src/views/AgentChat/index.js @@ -2,16 +2,11 @@ // 超炫酷的 AI 投研助手 - HeroUI v3 现代深色主题版本 // 使用 Framer Motion 物理动画引擎 + 毛玻璃效果 -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, Flex, useToast, useColorMode } from '@chakra-ui/react'; import { useAuth } from '@contexts/AuthContext'; -import { logger } from '@utils/logger'; -import axios from 'axios'; - -// 注意:图标导入已移至子组件中,主组件不再需要导入 // 常量配置 - 从 TypeScript 模块导入 -import { MessageTypes } from './constants/messageTypes'; import { DEFAULT_MODEL_ID } from './constants/models'; import { DEFAULT_SELECTED_TOOLS } from './constants/tools'; @@ -21,44 +16,78 @@ import LeftSidebar from './components/LeftSidebar'; import ChatArea from './components/ChatArea'; import RightSidebar from './components/RightSidebar'; +// 自定义 Hooks +import { useAgentSessions, useAgentChat, useFileUpload, useAutoScroll } from './hooks'; + /** * Agent Chat - 主组件(HeroUI v3 深色主题) * - * 注意:所有常量配置已提取到 constants/ 目录: - * - animations: constants/animations.ts - * - MessageTypes: constants/messageTypes.ts - * - AVAILABLE_MODELS: constants/models.ts - * - MCP_TOOLS, TOOL_CATEGORIES: constants/tools.ts - * - quickQuestions: constants/quickQuestions.ts + * 架构说明: + * - Phase 1: 常量配置已提取到 constants/ 目录(TypeScript) + * - Phase 2: UI 组件已拆分到 components/ 目录 + * - Phase 3: 业务逻辑已提取到 hooks/ 目录(TypeScript) + * + * 主组件职责: + * 1. 组合各个自定义 Hooks + * 2. 管理 UI 状态(侧边栏开关、模型选择、工具选择) + * 3. 组合渲染子组件 */ const AgentChat = () => { const { user } = useAuth(); const toast = useToast(); const { setColorMode } = useColorMode(); - // 会话管理 - const [sessions, setSessions] = useState([]); - const [currentSessionId, setCurrentSessionId] = useState(null); - const [isLoadingSessions, setIsLoadingSessions] = useState(false); - - // 消息管理 - const [messages, setMessages] = useState([]); - const [inputValue, setInputValue] = useState(''); - const [isProcessing, setIsProcessing] = useState(false); - - // UI 状态 + // ==================== UI 状态(主组件管理)==================== const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL_ID); const [selectedTools, setSelectedTools] = useState(DEFAULT_SELECTED_TOOLS); const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(true); const [isRightSidebarOpen, setIsRightSidebarOpen] = useState(true); - // 文件上传 - const [uploadedFiles, setUploadedFiles] = useState([]); - const fileInputRef = useRef(null); + // ==================== 自定义 Hooks ==================== - // Refs - const messagesEndRef = useRef(null); - const inputRef = useRef(null); + // 文件上传 Hook + const { uploadedFiles, fileInputRef, handleFileSelect, removeFile, clearFiles } = useFileUpload(); + + // 会话管理 Hook(需要先创建 messages state) + const [messages, setMessages] = useState([]); + + const { + sessions, + currentSessionId, + setCurrentSessionId, + isLoadingSessions, + loadSessions, + switchSession, + createNewSession, + } = useAgentSessions({ + user, + setMessages, + }); + + // 消息处理 Hook + const { + inputValue, + setInputValue, + isProcessing, + handleSendMessage, + handleKeyPress, + } = useAgentChat({ + user, + currentSessionId, + setCurrentSessionId, + selectedModel, + selectedTools, + uploadedFiles, + clearFiles, + toast, + loadSessions, + }); + + // 自动滚动 Hook + const { messagesEndRef } = useAutoScroll(messages); + + // ==================== 输入框引用(保留在主组件)==================== + const inputRef = React.useRef(null); // ==================== 启用深色模式 ==================== useEffect(() => { @@ -72,215 +101,7 @@ const AgentChat = () => { }; }, [setColorMode]); - // ==================== API 调用函数 ==================== - - const loadSessions = async () => { - if (!user?.id) return; - setIsLoadingSessions(true); - try { - const response = await axios.get('/mcp/agent/sessions', { - params: { user_id: user.id, limit: 50 }, - }); - if (response.data.success) { - setSessions(response.data.data); - } - } catch (error) { - logger.error('加载会话列表失败', error); - } finally { - setIsLoadingSessions(false); - } - }; - - const loadSessionHistory = async (sessionId) => { - if (!sessionId) return; - try { - const response = await axios.get(`/mcp/agent/history/${sessionId}`, { - params: { limit: 100 }, - }); - if (response.data.success) { - const history = response.data.data; - const formattedMessages = history.map((msg, idx) => ({ - id: `${sessionId}-${idx}`, - type: msg.message_type === 'user' ? MessageTypes.USER : MessageTypes.AGENT_RESPONSE, - content: msg.message, - plan: msg.plan ? JSON.parse(msg.plan) : null, - stepResults: msg.steps ? JSON.parse(msg.steps) : null, - timestamp: msg.timestamp, - })); - setMessages(formattedMessages); - } - } catch (error) { - logger.error('加载会话历史失败', error); - } - }; - - const createNewSession = () => { - setCurrentSessionId(null); - setMessages([ - { - id: Date.now(), - type: MessageTypes.AGENT_RESPONSE, - content: `你好${user?.nickname || ''}!👋\n\n我是**价小前**,你的 AI 投研助手。\n\n**我能做什么?**\n• 📊 全面分析股票基本面和技术面\n• 🔥 追踪市场热点和涨停板块\n• 📈 研究行业趋势和投资机会\n• 📰 汇总最新财经新闻和研报\n\n直接输入你的问题开始探索!`, - timestamp: new Date().toISOString(), - }, - ]); - }; - - const switchSession = (sessionId) => { - setCurrentSessionId(sessionId); - loadSessionHistory(sessionId); - }; - - const handleSendMessage = async () => { - if (!inputValue.trim() || isProcessing) return; - - const userMessage = { - type: MessageTypes.USER, - content: inputValue, - timestamp: new Date().toISOString(), - files: uploadedFiles.length > 0 ? uploadedFiles : undefined, - }; - - addMessage(userMessage); - const userInput = inputValue; - setInputValue(''); - setUploadedFiles([]); - setIsProcessing(true); - - try { - addMessage({ - type: MessageTypes.AGENT_THINKING, - content: '正在分析你的问题...', - timestamp: new Date().toISOString(), - }); - - const response = await axios.post('/mcp/agent/chat', { - message: userInput, - conversation_history: messages - .filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) - .map((m) => ({ - isUser: m.type === MessageTypes.USER, - content: m.content, - })), - user_id: user?.id || 'anonymous', - user_nickname: user?.nickname || '匿名用户', - user_avatar: user?.avatar || '', - subscription_type: user?.subscription_type || 'free', - session_id: currentSessionId, - model: selectedModel, - tools: selectedTools, - files: uploadedFiles.length > 0 ? uploadedFiles : undefined, - }); - - setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); - - if (response.data.success) { - const data = response.data; - if (data.session_id && !currentSessionId) { - setCurrentSessionId(data.session_id); - } - - if (data.plan) { - addMessage({ - type: MessageTypes.AGENT_PLAN, - content: '已制定执行计划', - plan: data.plan, - timestamp: new Date().toISOString(), - }); - } - - if (data.steps && data.steps.length > 0) { - addMessage({ - type: MessageTypes.AGENT_EXECUTING, - content: '正在执行步骤...', - plan: data.plan, - stepResults: data.steps, - timestamp: new Date().toISOString(), - }); - } - - setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); - - addMessage({ - type: MessageTypes.AGENT_RESPONSE, - content: data.final_answer || data.message || '处理完成', - plan: data.plan, - stepResults: data.steps, - metadata: data.metadata, - timestamp: new Date().toISOString(), - }); - - loadSessions(); - } - } catch (error) { - logger.error('Agent chat error', error); - setMessages((prev) => - prev.filter( - (m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING - ) - ); - - const errorMessage = error.response?.data?.error || error.message || '处理失败'; - addMessage({ - type: MessageTypes.ERROR, - content: `处理失败:${errorMessage}`, - timestamp: new Date().toISOString(), - }); - - toast({ - title: '处理失败', - description: errorMessage, - status: 'error', - duration: 5000, - }); - } finally { - setIsProcessing(false); - } - }; - - // 文件上传处理 - const handleFileSelect = (event) => { - const files = Array.from(event.target.files || []); - const fileData = files.map(file => ({ - name: file.name, - size: file.size, - type: file.type, - // 实际上传时需要转换为 base64 或上传到服务器 - url: URL.createObjectURL(file), - })); - setUploadedFiles(prev => [...prev, ...fileData]); - }; - - const removeFile = (index) => { - setUploadedFiles(prev => prev.filter((_, i) => i !== index)); - }; - - const addMessage = (message) => { - setMessages((prev) => [ - ...prev, - { - id: Date.now() + Math.random(), - ...message, - }, - ]); - }; - - const handleKeyPress = (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - }; - - useEffect(() => { - loadSessions(); - createNewSession(); - }, [user]); - - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages]); - + // ==================== 渲染组件 ==================== return (