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 MCPChat from "@/components/Chat/MCPChat";
|
||||
// 使用原始模板的漂亮界面
|
||||
import HomePage from "@/templates/HomePage";
|
||||
// import MCPChat from "@/components/Chat/MCPChat";
|
||||
|
||||
export default function Page() {
|
||||
// 直接显示聊天界面
|
||||
return (
|
||||
<main className="h-screen">
|
||||
<MCPChat />
|
||||
</main>
|
||||
);
|
||||
// 使用原始主页 - 漂亮的界面
|
||||
return <HomePage />;
|
||||
|
||||
// 或者使用原始主页
|
||||
// return <HomePage />;
|
||||
// 简单聊天界面(备用)
|
||||
// return (
|
||||
// <main className="h-screen">
|
||||
// <MCPChat />
|
||||
// </main>
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -1,305 +1,62 @@
|
||||
// components/Chat/MCPChat.tsx - 集成 MCP 的聊天组件
|
||||
'use client';
|
||||
import MCPPanelMessage from "@/components/PanelMessage/MCPPanelMessage";
|
||||
import Head from "./Head";
|
||||
import { useState } from "react";
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { mcpService } from '../../services/mcp-real';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import styles from './MCPChat.module.css';
|
||||
type Props = {
|
||||
titleHead?: React.ReactNode;
|
||||
hidePanelMessage?: boolean;
|
||||
children: React.ReactNode;
|
||||
onInputChange?: (value: string) => void;
|
||||
onSend?: () => void;
|
||||
inputValue?: string;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
const MCPChat = ({
|
||||
titleHead,
|
||||
hidePanelMessage,
|
||||
children,
|
||||
onInputChange,
|
||||
onSend,
|
||||
inputValue,
|
||||
isLoading
|
||||
}: Props) => {
|
||||
const [localInput, setLocalInput] = useState('');
|
||||
|
||||
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(),
|
||||
const handleInputChange = (value: string) => {
|
||||
if (onInputChange) {
|
||||
onInputChange(value);
|
||||
} else {
|
||||
setLocalInput(value);
|
||||
}
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
setStreamingMessage('');
|
||||
const handleSend = () => {
|
||||
if (onSend) {
|
||||
onSend();
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
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 className="chat-wrapper">
|
||||
<Head title={titleHead} />
|
||||
<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'
|
||||
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 ${
|
||||
hidePanelMessage ? "" : "-mb-3 pb-10"
|
||||
}`}
|
||||
>
|
||||
{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>
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
{!hidePanelMessage && (
|
||||
<MCPPanelMessage
|
||||
value={inputValue || localInput}
|
||||
onInputChange={handleInputChange}
|
||||
onSend={handleSend}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</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 模块文件需要单独创建
|
||||
export default MCPChat;
|
||||
@@ -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