update pay function
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
// app/ai-chat/generate-code-template/page.tsx - 原始模板页面(仅演示)
|
||||||
|
import GenerateCodePage from "@/templates/GenerateCodePage";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <GenerateCodePage />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// app/ai-chat/generate-code/page.tsx - MCP 集成的代码生成页面
|
||||||
|
import MCPGenerateCodePage from "@/templates/GenerateCodePage/MCPIntegrated";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <MCPGenerateCodePage />;
|
||||||
|
}
|
||||||
7
src/views/AgentChat/neuratalk/app/chat/page.tsx
Normal file
7
src/views/AgentChat/neuratalk/app/chat/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// app/chat/page.tsx - 使用模板的漂亮聊天界面
|
||||||
|
import GenerateCodePage from "@/templates/GenerateCodePage";
|
||||||
|
|
||||||
|
export default function ChatPage() {
|
||||||
|
// 使用代码生成页面模板(最接近聊天功能)
|
||||||
|
return <GenerateCodePage />;
|
||||||
|
}
|
||||||
75
src/views/AgentChat/neuratalk/app/demo/page.tsx
Normal file
75
src/views/AgentChat/neuratalk/app/demo/page.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// app/demo/page.tsx - 所有页面演示
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
export default function DemoPage() {
|
||||||
|
const pages = [
|
||||||
|
{ name: '🏠 主页', path: '/ai-chat', desc: '欢迎页面,功能卡片展示' },
|
||||||
|
{ name: '🤖 AI 代码助手', path: '/ai-chat/generate-code', desc: '✨ 漂亮界面 + MCP 集成(推荐)' },
|
||||||
|
{ name: '💬 模板代码生成', path: '/ai-chat/generate-code-template', desc: '原始模板界面(仅演示)' },
|
||||||
|
{ name: '🎨 图像生成', path: '/ai-chat/image-generation', desc: '图像生成界面' },
|
||||||
|
{ name: '📊 数据分析', path: '/ai-chat/data-analytics', desc: '数据分析和图表' },
|
||||||
|
{ name: '📝 文档生成', path: '/ai-chat/document-generation', desc: '文档生成界面' },
|
||||||
|
{ name: '📁 文档管理', path: '/ai-chat/documents', desc: '文档列表管理' },
|
||||||
|
{ name: '🔍 研究', path: '/ai-chat/research', desc: '研究和语音功能' },
|
||||||
|
{ name: '📋 模板', path: '/ai-chat/templates', desc: '模板库' },
|
||||||
|
{ name: '⏰ 历史', path: '/ai-chat/history', desc: '聊天历史' },
|
||||||
|
{ name: '🚀 简单聊天', path: '/ai-chat/chat-direct', desc: '简化版聊天(已集成 MCP)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-800 mb-2">AI Chat 页面导航</h1>
|
||||||
|
<p className="text-gray-600 mb-8">选择您想查看的页面模板</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{pages.map((page) => (
|
||||||
|
<a
|
||||||
|
key={page.path}
|
||||||
|
href={page.path}
|
||||||
|
className="bg-white rounded-lg shadow-lg hover:shadow-xl transition-shadow p-6 block"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-semibold mb-2">{page.name}</h2>
|
||||||
|
<p className="text-gray-600 mb-4">{page.desc}</p>
|
||||||
|
<span className="text-blue-600 hover:text-blue-800 font-medium">
|
||||||
|
访问页面 →
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-12 p-6 bg-white rounded-lg shadow">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">推荐查看</h2>
|
||||||
|
<ul className="space-y-2 text-gray-700">
|
||||||
|
<li>
|
||||||
|
<strong>最漂亮:</strong>
|
||||||
|
<a href="/ai-chat" className="text-blue-600 hover:underline">主页</a>
|
||||||
|
- 精美的欢迎界面和功能卡片
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>🔥 最新集成:</strong>
|
||||||
|
<a href="/ai-chat/generate-code" className="text-blue-600 hover:underline">AI 代码助手</a>
|
||||||
|
- 漂亮界面 + MCP 功能
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>最全功能:</strong>
|
||||||
|
<a href="/ai-chat/research" className="text-blue-600 hover:underline">研究</a>
|
||||||
|
- 包含语音和多种工具
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>已集成 MCP:</strong>
|
||||||
|
<a href="/ai-chat/chat-direct" className="text-blue-600 hover:underline">直接聊天</a>
|
||||||
|
- 简化版但已连接 MCP
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 text-center">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
当前用户:<span className="font-bold">星野源 (Max)</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
// 可以选择使用原始主页或直接进入聊天
|
// 使用原始模板的漂亮界面
|
||||||
// import HomePage from "@/templates/HomePage";
|
import HomePage from "@/templates/HomePage";
|
||||||
import MCPChat from "@/components/Chat/MCPChat";
|
// import MCPChat from "@/components/Chat/MCPChat";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
// 直接显示聊天界面
|
// 使用原始主页 - 漂亮的界面
|
||||||
return (
|
return <HomePage />;
|
||||||
<main className="h-screen">
|
|
||||||
<MCPChat />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 或者使用原始主页
|
// 简单聊天界面(备用)
|
||||||
// return <HomePage />;
|
// return (
|
||||||
|
// <main className="h-screen">
|
||||||
|
// <MCPChat />
|
||||||
|
// </main>
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,305 +1,62 @@
|
|||||||
// components/Chat/MCPChat.tsx - 集成 MCP 的聊天组件
|
import MCPPanelMessage from "@/components/PanelMessage/MCPPanelMessage";
|
||||||
'use client';
|
import Head from "./Head";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
type Props = {
|
||||||
import { mcpService } from '../../services/mcp-real';
|
titleHead?: React.ReactNode;
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
hidePanelMessage?: boolean;
|
||||||
import styles from './MCPChat.module.css';
|
children: React.ReactNode;
|
||||||
|
onInputChange?: (value: string) => void;
|
||||||
|
onSend?: () => void;
|
||||||
|
inputValue?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
interface Message {
|
const MCPChat = ({
|
||||||
id: string;
|
titleHead,
|
||||||
role: 'user' | 'assistant' | 'system';
|
hidePanelMessage,
|
||||||
content: string;
|
children,
|
||||||
timestamp: Date;
|
onInputChange,
|
||||||
isStreaming?: boolean;
|
onSend,
|
||||||
}
|
inputValue,
|
||||||
|
isLoading
|
||||||
|
}: Props) => {
|
||||||
|
const [localInput, setLocalInput] = useState('');
|
||||||
|
|
||||||
export default function MCPChat() {
|
const handleInputChange = (value: string) => {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
if (onInputChange) {
|
||||||
const [input, setInput] = useState('');
|
onInputChange(value);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
} else {
|
||||||
const [streamingMessage, setStreamingMessage] = useState('');
|
setLocalInput(value);
|
||||||
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]);
|
const handleSend = () => {
|
||||||
setInput('');
|
if (onSend) {
|
||||||
setIsLoading(true);
|
onSend();
|
||||||
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 (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="chat-wrapper">
|
||||||
<div className="text-lg">加载中...</div>
|
<Head title={titleHead} />
|
||||||
</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) {
|
|
||||||
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">需要订阅才能使用 AI 助手</h2>
|
|
||||||
<p className="text-gray-600 mb-4">升级到高级版解锁所有功能</p>
|
|
||||||
<p className="text-sm text-gray-500 mb-6">
|
|
||||||
当前订阅等级:{user?.subscription_type || user?.subscription_tier || '未知'}
|
|
||||||
{user?.is_subscription_active && ' (活跃)'}
|
|
||||||
</p>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<a
|
|
||||||
href={`${mainAppUrl}/subscription`}
|
|
||||||
className="block bg-green-600 text-white px-6 py-2 rounded hover:bg-green-700"
|
|
||||||
>
|
|
||||||
查看订阅方案
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
className="block bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
刷新页面
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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 || user?.nickname}</div>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{user?.subscription_type || user?.subscription_tier || 'Free'}
|
|
||||||
{user?.is_subscription_active && ' ✓'}
|
|
||||||
</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
|
<div
|
||||||
key={msg.id}
|
className={`flex flex-col gap-4.5 grow p-7.5 overflow-auto scrollbar-none max-md:gap-3 max-md:p-4 max-md:pb-8 ${
|
||||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
hidePanelMessage ? "" : "-mb-3 pb-10"
|
||||||
>
|
|
||||||
<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 ? (
|
{children}
|
||||||
<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>
|
||||||
))}
|
{!hidePanelMessage && (
|
||||||
<div ref={messagesEndRef} />
|
<MCPPanelMessage
|
||||||
|
value={inputValue || localInput}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
onSend={handleSend}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
{/* 输入区域 */}
|
export default MCPChat;
|
||||||
<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 模块文件需要单独创建
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { useState, useEffect, KeyboardEvent } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import TextareaAutosize from "react-textarea-autosize";
|
||||||
|
import Icon from "@/components/Icon";
|
||||||
|
import Image from "@/components/Image";
|
||||||
|
import Note from "./Note";
|
||||||
|
import Speed from "./Speed";
|
||||||
|
import Search from "./Search";
|
||||||
|
import Menu from "./Menu";
|
||||||
|
|
||||||
|
type ButtonProps = {
|
||||||
|
className?: string;
|
||||||
|
icon: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Button = ({ className, icon, onClick }: ButtonProps) => {
|
||||||
|
return (
|
||||||
|
<button className={`group text-0 ${className || ""}`} onClick={onClick}>
|
||||||
|
<Icon
|
||||||
|
className="fill-icon-soft-400 transition-colors group-hover:fill-blue-500"
|
||||||
|
name={icon}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type MCPPanelMessageProps = {
|
||||||
|
value?: string;
|
||||||
|
onInputChange?: (value: string) => void;
|
||||||
|
onSend?: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MCPPanelMessage = ({
|
||||||
|
value = '',
|
||||||
|
onInputChange,
|
||||||
|
onSend,
|
||||||
|
isLoading = false,
|
||||||
|
placeholder = "输入消息,与 AI 对话..."
|
||||||
|
}: MCPPanelMessageProps) => {
|
||||||
|
const [localMessage, setLocalMessage] = useState("");
|
||||||
|
|
||||||
|
// 同步外部值
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setLocalMessage(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleChange = (newValue: string) => {
|
||||||
|
setLocalMessage(newValue);
|
||||||
|
if (onInputChange) {
|
||||||
|
onInputChange(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
if (!localMessage.trim() || isLoading) return;
|
||||||
|
|
||||||
|
if (onSend) {
|
||||||
|
onSend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
// Shift+Enter 换行,Enter 发送
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative z-3 mx-7.5 mb-5.5 shrink-0 rounded-xl border border-stroke-soft-200 bg-white-0 max-md:m-0">
|
||||||
|
<Note />
|
||||||
|
<div className="px-3 py-3.5 max-md:px-4 max-md:py-2.5">
|
||||||
|
<div className="min-h-12 text-0 mb-3">
|
||||||
|
<TextareaAutosize
|
||||||
|
className="w-full h-12 text-p-md text-strong-950 outline-none resize-none placeholder:text-soft-400 disabled:opacity-50"
|
||||||
|
maxRows={5}
|
||||||
|
value={localMessage}
|
||||||
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<Speed />
|
||||||
|
<div className="w-0.25 h-5 bg-stroke-soft-200"></div>
|
||||||
|
<Menu />
|
||||||
|
<Button icon="link" onClick={() => {}} />
|
||||||
|
<Search />
|
||||||
|
<Button icon="image" onClick={() => {}} />
|
||||||
|
<Link className="group text-0" href="/ai-chat/research">
|
||||||
|
<Icon
|
||||||
|
className="fill-icon-soft-400 transition-colors group-hover:fill-blue-500"
|
||||||
|
name="voice"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<div className="w-0.25 h-5 bg-stroke-soft-200"></div>
|
||||||
|
<button
|
||||||
|
className={`group text-0 ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={isLoading || !localMessage.trim()}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
className="w-5 opacity-100"
|
||||||
|
src="/images/sent.svg"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
alt="Sent"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* 快捷提示 */}
|
||||||
|
<div className="mt-2 text-xs text-soft-400">
|
||||||
|
按 Enter 发送,Shift+Enter 换行
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MCPPanelMessage;
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import Layout from "@/components/Layout";
|
||||||
|
import MCPChat from "@/components/Chat/MCPChat";
|
||||||
|
import Question from "@/components/Question";
|
||||||
|
import Answer from "@/components/Answer";
|
||||||
|
import CodeEditor from "@/components/CodeEditor";
|
||||||
|
import { mcpService } from '@/services/mcp-real';
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
code?: {
|
||||||
|
language: string;
|
||||||
|
content: string;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
isStreaming?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MCPGenerateCodePage = () => {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [currentStreamingContent, setCurrentStreamingContent] = useState('');
|
||||||
|
const [currentStreamingId, setCurrentStreamingId] = useState<string | null>(null);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { user, isAuthenticated, canAccessChat, loading: authLoading } = useAuth();
|
||||||
|
|
||||||
|
// 自动滚动到底部
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, [messages, currentStreamingContent]);
|
||||||
|
|
||||||
|
const extractCodeFromResponse = (content: string) => {
|
||||||
|
// 匹配 markdown 代码块
|
||||||
|
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
||||||
|
const matches = [...content.matchAll(codeBlockRegex)];
|
||||||
|
|
||||||
|
if (matches.length > 0) {
|
||||||
|
// 返回第一个代码块
|
||||||
|
const [, language = 'javascript', code] = matches[0];
|
||||||
|
return {
|
||||||
|
hasCode: true,
|
||||||
|
language,
|
||||||
|
code: code.trim(),
|
||||||
|
textBefore: content.substring(0, matches[0].index),
|
||||||
|
textAfter: content.substring((matches[0].index || 0) + matches[0][0].length)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hasCode: false, content };
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
setCurrentStreamingContent('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建助手消息占位符
|
||||||
|
const assistantMessageId = (Date.now() + 1).toString();
|
||||||
|
setCurrentStreamingId(assistantMessageId);
|
||||||
|
|
||||||
|
const assistantMessage: Message = {
|
||||||
|
id: assistantMessageId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
timestamp: new Date(),
|
||||||
|
isStreaming: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, assistantMessage]);
|
||||||
|
|
||||||
|
// 流式接收回复
|
||||||
|
let fullContent = '';
|
||||||
|
for await (const chunk of mcpService.streamMessage(input)) {
|
||||||
|
fullContent += chunk;
|
||||||
|
setCurrentStreamingContent(fullContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查回复中是否包含代码
|
||||||
|
const codeInfo = extractCodeFromResponse(fullContent);
|
||||||
|
|
||||||
|
// 更新最终消息
|
||||||
|
setMessages(prev =>
|
||||||
|
prev.map(msg =>
|
||||||
|
msg.id === assistantMessageId
|
||||||
|
? {
|
||||||
|
...msg,
|
||||||
|
content: codeInfo.hasCode
|
||||||
|
? (codeInfo.textBefore || '这是生成的代码:')
|
||||||
|
: fullContent,
|
||||||
|
code: codeInfo.hasCode
|
||||||
|
? {
|
||||||
|
language: codeInfo.language!,
|
||||||
|
content: codeInfo.code!,
|
||||||
|
title: '生成的代码'
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
isStreaming: false
|
||||||
|
}
|
||||||
|
: msg
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setCurrentStreamingContent('');
|
||||||
|
setCurrentStreamingId(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Send message error:', error);
|
||||||
|
setMessages(prev => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: Date.now().toString(),
|
||||||
|
role: 'system',
|
||||||
|
content: '抱歉,发送消息时出错了。请稍后重试。',
|
||||||
|
timestamp: new Date(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
if (authLoading) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="flex items-center justify-center h-96">
|
||||||
|
<div className="text-lg">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
const mainAppUrl = process.env.NEXT_PUBLIC_MAIN_APP_URL || 'https://valuefrontier.cn';
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="flex flex-col items-center justify-center h-96">
|
||||||
|
<h2 className="text-2xl mb-4">请先登录</h2>
|
||||||
|
<a
|
||||||
|
href={`${mainAppUrl}/auth/sign-in?redirect=/ai-chat/generate-code`}
|
||||||
|
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
前往登录
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canAccessChat) {
|
||||||
|
const mainAppUrl = process.env.NEXT_PUBLIC_MAIN_APP_URL || 'https://valuefrontier.cn';
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="flex flex-col items-center justify-center h-96">
|
||||||
|
<h2 className="text-2xl mb-4">需要订阅才能使用 AI 助手</h2>
|
||||||
|
<p className="text-gray-600 mb-4">升级到高级版解锁所有功能</p>
|
||||||
|
<a
|
||||||
|
href={`${mainAppUrl}/subscription`}
|
||||||
|
className="bg-green-600 text-white px-6 py-2 rounded hover:bg-green-700"
|
||||||
|
>
|
||||||
|
查看订阅方案
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<MCPChat
|
||||||
|
onInputChange={setInput}
|
||||||
|
onSend={handleSend}
|
||||||
|
inputValue={input}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="text-center text-gray-500 py-12">
|
||||||
|
<h3 className="text-xl mb-4">👋 欢迎使用 AI 代码生成助手</h3>
|
||||||
|
<p>输入您的需求,我会帮您生成代码</p>
|
||||||
|
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-3 max-w-2xl mx-auto">
|
||||||
|
<button
|
||||||
|
onClick={() => setInput('生成一个响应式的导航栏组件')}
|
||||||
|
className="text-left p-3 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
💡 生成响应式导航栏
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setInput('创建一个表单验证函数')}
|
||||||
|
className="text-left p-3 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
📝 创建表单验证
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setInput('写一个排序算法')}
|
||||||
|
className="text-left p-3 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
🔢 实现排序算法
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setInput('生成 REST API 接口')}
|
||||||
|
className="text-left p-3 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
🌐 创建 API 接口
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{messages.map((msg, index) => (
|
||||||
|
<div key={msg.id}>
|
||||||
|
{msg.role === 'user' ? (
|
||||||
|
<Question>{msg.content}</Question>
|
||||||
|
) : msg.role === 'assistant' ? (
|
||||||
|
<Answer>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{msg.isStreaming ? (
|
||||||
|
<div className="whitespace-pre-wrap">
|
||||||
|
{msg.id === currentStreamingId ? currentStreamingContent : msg.content}
|
||||||
|
{msg.id === currentStreamingId && !currentStreamingContent && (
|
||||||
|
<span className="inline-block animate-pulse">思考中...</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{msg.content && (
|
||||||
|
<div className="whitespace-pre-wrap">{msg.content}</div>
|
||||||
|
)}
|
||||||
|
{msg.code && (
|
||||||
|
<CodeEditor
|
||||||
|
title={msg.code.title || '生成的代码'}
|
||||||
|
language={msg.code.language}
|
||||||
|
initialCode={msg.code.content}
|
||||||
|
onCodeChange={() => {}}
|
||||||
|
isGenerating={false}
|
||||||
|
readOnly={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Answer>
|
||||||
|
) : (
|
||||||
|
<div className="px-4 py-2 bg-yellow-50 text-yellow-800 rounded-lg">
|
||||||
|
{msg.content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MCPChat>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MCPGenerateCodePage;
|
||||||
Reference in New Issue
Block a user