feat: 拆分左侧栏、中间聊天区、右侧栏组件, Hooks 提取
This commit is contained in:
@@ -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 (
|
||||
<Box flex={1} bg="gray.900">
|
||||
<Flex h="100%" overflow="hidden" position="relative">
|
||||
|
||||
Reference in New Issue
Block a user