289 lines
9.1 KiB
TypeScript
289 lines
9.1 KiB
TypeScript
// components/Chat/MCPChat.tsx - 集成 MCP 的聊天组件
|
|
'use client';
|
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
import { mcpService } from '../../services/mcp-real';
|
|
import { useAuth } from '../../hooks/useAuth';
|
|
import styles from './MCPChat.module.css';
|
|
|
|
interface Message {
|
|
id: string;
|
|
role: 'user' | 'assistant' | 'system';
|
|
content: string;
|
|
timestamp: Date;
|
|
isStreaming?: boolean;
|
|
}
|
|
|
|
export default function MCPChat() {
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
const [input, setInput] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [streamingMessage, setStreamingMessage] = useState('');
|
|
const [tools, setTools] = useState<any[]>([]);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
const { user, isAuthenticated, canAccessChat, loading: authLoading } = useAuth();
|
|
|
|
// 加载可用工具
|
|
useEffect(() => {
|
|
if (isAuthenticated && canAccessChat) {
|
|
loadTools();
|
|
}
|
|
}, [isAuthenticated, canAccessChat]);
|
|
|
|
// 自动滚动到底部
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}, [messages, streamingMessage]);
|
|
|
|
const loadTools = async () => {
|
|
try {
|
|
const availableTools = await mcpService.getTools();
|
|
setTools(availableTools);
|
|
console.log('Available tools:', availableTools);
|
|
} catch (error) {
|
|
console.error('Failed to load tools:', error);
|
|
}
|
|
};
|
|
|
|
const handleSend = async () => {
|
|
if (!input.trim() || isLoading) return;
|
|
|
|
const userMessage: Message = {
|
|
id: Date.now().toString(),
|
|
role: 'user',
|
|
content: input,
|
|
timestamp: new Date(),
|
|
};
|
|
|
|
setMessages(prev => [...prev, userMessage]);
|
|
setInput('');
|
|
setIsLoading(true);
|
|
setStreamingMessage('');
|
|
|
|
try {
|
|
// 创建助手消息占位符
|
|
const assistantMessage: Message = {
|
|
id: (Date.now() + 1).toString(),
|
|
role: 'assistant',
|
|
content: '',
|
|
timestamp: new Date(),
|
|
isStreaming: true,
|
|
};
|
|
|
|
setMessages(prev => [...prev, assistantMessage]);
|
|
|
|
// 流式接收回复
|
|
let fullContent = '';
|
|
for await (const chunk of mcpService.streamMessage(input)) {
|
|
fullContent += chunk;
|
|
setStreamingMessage(fullContent);
|
|
}
|
|
|
|
// 更新最终消息
|
|
setMessages(prev =>
|
|
prev.map(msg =>
|
|
msg.id === assistantMessage.id
|
|
? { ...msg, content: fullContent, isStreaming: false }
|
|
: msg
|
|
)
|
|
);
|
|
setStreamingMessage('');
|
|
} catch (error) {
|
|
console.error('Send message error:', error);
|
|
setMessages(prev => [
|
|
...prev,
|
|
{
|
|
id: Date.now().toString(),
|
|
role: 'system',
|
|
content: '抱歉,发送消息时出错了。请稍后重试。',
|
|
timestamp: new Date(),
|
|
},
|
|
]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleToolCall = async (toolName: string) => {
|
|
// 示例:调用工具
|
|
try {
|
|
setIsLoading(true);
|
|
const result = await mcpService.callTool(toolName, {});
|
|
console.log('Tool result:', result);
|
|
|
|
// 显示工具结果
|
|
setMessages(prev => [
|
|
...prev,
|
|
{
|
|
id: Date.now().toString(),
|
|
role: 'system',
|
|
content: `工具 ${toolName} 执行结果:${JSON.stringify(result.result, null, 2)}`,
|
|
timestamp: new Date(),
|
|
},
|
|
]);
|
|
} catch (error) {
|
|
console.error('Tool call error:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// 权限检查
|
|
if (authLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-screen">
|
|
<div className="text-lg">加载中...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
const mainAppUrl = process.env.NEXT_PUBLIC_MAIN_APP_URL || 'https://valuefrontier.cn';
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-screen">
|
|
<h2 className="text-2xl mb-4">请先登录</h2>
|
|
<a
|
|
href={`${mainAppUrl}/auth/sign-in?redirect=/ai-chat`}
|
|
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
|
|
>
|
|
前往登录
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!canAccessChat) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-screen">
|
|
<h2 className="text-2xl mb-4">需要订阅才能使用 AI 助手</h2>
|
|
<p className="text-gray-600 mb-6">升级到高级版解锁所有功能</p>
|
|
<a
|
|
href={`${process.env.NEXT_PUBLIC_MAIN_APP_URL}/subscription`}
|
|
className="bg-green-600 text-white px-6 py-2 rounded hover:bg-green-700"
|
|
>
|
|
查看订阅方案
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
|
{/* 侧边栏 - 工具列表 */}
|
|
<div className="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 p-4">
|
|
<h3 className="font-semibold mb-4">可用工具</h3>
|
|
<div className="space-y-2">
|
|
{tools.map(tool => (
|
|
<button
|
|
key={tool.name}
|
|
onClick={() => handleToolCall(tool.name)}
|
|
disabled={isLoading}
|
|
className="w-full text-left p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
|
|
>
|
|
<div className="font-medium">{tool.name}</div>
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
{tool.description}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* 用户信息 */}
|
|
<div className="mt-auto pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white">
|
|
{user?.username?.[0]?.toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<div className="font-medium">{user?.username}</div>
|
|
<div className="text-xs text-gray-500">{user?.subscription_tier}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 主聊天区域 */}
|
|
<div className="flex-1 flex flex-col">
|
|
{/* 消息列表 */}
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
{messages.map(msg => (
|
|
<div
|
|
key={msg.id}
|
|
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
>
|
|
<div
|
|
className={`max-w-2xl px-4 py-2 rounded-lg ${
|
|
msg.role === 'user'
|
|
? 'bg-blue-600 text-white'
|
|
: msg.role === 'system'
|
|
? 'bg-yellow-100 text-yellow-800'
|
|
: 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
|
}`}
|
|
>
|
|
{msg.isStreaming ? (
|
|
<div>
|
|
{streamingMessage || (
|
|
<span className="inline-block animate-pulse">思考中...</span>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="whitespace-pre-wrap">{msg.content}</div>
|
|
)}
|
|
<div className="text-xs mt-1 opacity-70">
|
|
{msg.timestamp.toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
{/* 输入区域 */}
|
|
<div className="border-t border-gray-200 dark:border-gray-700 p-4">
|
|
<div className="flex space-x-2">
|
|
<input
|
|
type="text"
|
|
value={input}
|
|
onChange={e => setInput(e.target.value)}
|
|
onKeyPress={e => e.key === 'Enter' && handleSend()}
|
|
disabled={isLoading}
|
|
placeholder="输入消息..."
|
|
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-white"
|
|
/>
|
|
<button
|
|
onClick={handleSend}
|
|
disabled={!input.trim() || isLoading}
|
|
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isLoading ? '发送中...' : '发送'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* 快捷操作 */}
|
|
<div className="flex space-x-2 mt-2">
|
|
<button
|
|
onClick={() => setInput('/help')}
|
|
className="text-sm text-blue-600 hover:underline"
|
|
>
|
|
帮助
|
|
</button>
|
|
<button
|
|
onClick={() => setInput('/tools')}
|
|
className="text-sm text-blue-600 hover:underline"
|
|
>
|
|
查看工具
|
|
</button>
|
|
<button
|
|
onClick={() => mcpService.clearHistory()}
|
|
className="text-sm text-red-600 hover:underline"
|
|
>
|
|
清空历史
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// CSS 模块文件需要单独创建
|