update pay function

This commit is contained in:
2025-11-22 10:11:36 +08:00
parent 20c6356842
commit 71f3834b79
8 changed files with 560 additions and 304 deletions

View File

@@ -0,0 +1,6 @@
// app/ai-chat/generate-code-template/page.tsx - 原始模板页面(仅演示)
import GenerateCodePage from "@/templates/GenerateCodePage";
export default function Page() {
return <GenerateCodePage />;
}

View File

@@ -0,0 +1,6 @@
// app/ai-chat/generate-code/page.tsx - MCP 集成的代码生成页面
import MCPGenerateCodePage from "@/templates/GenerateCodePage/MCPIntegrated";
export default function Page() {
return <MCPGenerateCodePage />;
}

View File

@@ -0,0 +1,7 @@
// app/chat/page.tsx - 使用模板的漂亮聊天界面
import GenerateCodePage from "@/templates/GenerateCodePage";
export default function ChatPage() {
// 使用代码生成页面模板(最接近聊天功能)
return <GenerateCodePage />;
}

View 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>
);
}

View File

@@ -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>
// );
}

View File

@@ -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 handleInputChange = (value: string) => {
if (onInputChange) {
onInputChange(value);
} else {
setLocalInput(value);
}
};
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 handleSend = () => {
if (onSend) {
onSend();
}
};
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>
)}
{children}
</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"
{!hidePanelMessage && (
<MCPPanelMessage
value={inputValue || localInput}
onInputChange={handleInputChange}
onSend={handleSend}
isLoading={isLoading}
/>
<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;

View File

@@ -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;

View File

@@ -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;