Files
vf_react/src/views/AgentChat/hooks/useAgentChat.ts
2025-11-28 12:27:30 +08:00

289 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/AgentChat/hooks/useAgentChat.ts
// 消息处理 Hook - 发送消息、处理响应、错误处理
import { useState, useCallback } from 'react';
import type { Dispatch, SetStateAction, KeyboardEvent } from 'react';
import axios from 'axios';
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<SetStateAction<string | null>>;
/** 选中的 AI 模型 */
selectedModel: string;
/** 选中的工具列表 */
selectedTools: string[];
/** 已上传文件列表 */
uploadedFiles: UploadedFile[];
/** 清空已上传文件 */
clearFiles: () => void;
/** Toast 通知函数 */
toast: ToastFunction;
/** 重新加载会话列表(发送消息成功后调用) */
loadSessions: () => Promise<void>;
}
/**
* useAgentChat Hook 返回值
*/
export interface UseAgentChatReturn {
/** 消息列表 */
messages: Message[];
/** 设置消息列表 */
setMessages: Dispatch<SetStateAction<Message[]>>;
/** 输入框内容 */
inputValue: string;
/** 设置输入框内容 */
setInputValue: Dispatch<SetStateAction<string>>;
/** 是否正在处理消息 */
isProcessing: boolean;
/** 发送消息 */
handleSendMessage: () => Promise<void>;
/** 键盘事件处理Enter 发送) */
handleKeyPress: (e: KeyboardEvent<HTMLInputElement>) => void;
/** 添加消息到列表 */
addMessage: (message: Partial<Message>) => 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<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
/**
* 添加消息到列表
*/
const addMessage = useCallback((message: Partial<Message>) => {
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<Message> = {
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 ? String(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) {
console.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<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
},
[handleSendMessage]
);
return {
messages,
setMessages,
inputValue,
setInputValue,
isProcessing,
handleSendMessage,
handleKeyPress,
addMessage,
};
};