Files
agent/app/page.tsx
zch1234qq 4afe6529fb
Some checks failed
Docker Build and Deploy / build-and-push (push) Has been cancelled
Docker Build and Deploy / deploy-hk (push) Has been cancelled
qwe
2025-10-21 09:17:52 +08:00

501 lines
18 KiB
TypeScript
Raw Permalink 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.

"use client"
import React, { useState, useRef, useEffect } from 'react';
import { Settings, Help, Message as MessageIcon } from '@mui/icons-material';
import {
Box,
Paper,
Typography,
Container,
useMediaQuery,
useTheme,
Alert,
IconButton,
Snackbar,
Avatar
} from '@mui/material';
import type { Message as MessageType } from './types/types';
import { initializeCoze, getCozeService, isCozeInitialized } from './service/initializeCoze';
import type { CozeConfig } from './types/types';
import MuiThemeProvider from './components/MuiThemeProvider';
import Message from './components/Message';
import { MessageInput } from './components/MessageInput';
import { Sidebar } from './components/Sidebar';
// 欢迎消息
const welcomeMessage: MessageType = {
id: '1',
content: '终于等到你了!我是你的爆款文案策划师,开始之前,我需要你提供一些简单的信息,让我能给你更有针对性的运营方案。\n1. 你的行业/背调/产品/服务(即使没有现有产品,可以说想要的产品/服务)\n2. 你的目标用户(根据你从业经验描绘的目标用户)',
role: 'assistant',
content_type: 'text',
timestamp: new Date(Date.now() - 60000),
};
const ChatInterface: React.FC = () => {
// 状态管理
const [messages, setMessages] = useState<MessageType[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isConfigured, setIsConfigured] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showError, setShowError] = useState(false);
const [activeTab, setActiveTab] = useState<'chat' | 'help'>('chat');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// 聊天容器的引用,用于自动滚动到底部
const chatContainerRef = useRef<HTMLDivElement>(null);
// 组件挂载时使用提供的API密钥和Bot ID初始化Coze服务
useEffect(() => {
// 客户端初始化代码
// 使用用户提供的API密钥和Bot ID
const apiKey = 'sat_XLx0k9rgirVutzov6ADylmscgU0zXufCZJHU13xorno5d7JzVCSrFkl1kJpQsJX0';
const botId = '7560159608908939283'; // 从环境变量获取Bot ID
// 添加日志记录配置状态
console.log('Coze API Key provided:', !!apiKey);
console.log('Coze Bot ID:', botId);
if (apiKey && botId) {
const cozeConfig: CozeConfig = { apiKey, botId };
const initialized = initializeCoze(cozeConfig);
setIsConfigured(initialized);
console.log('Coze service initialization result:', initialized);
} else {
console.warn('Coze API 配置不完整');
}
}, []);
// 当消息更新时,自动滚动到底部
useEffect(() => {
if (chatContainerRef.current) {
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}
}, [messages]);
// 处理消息发送
const handleSendMessage = async () => {
if (!input.trim()) return;
// 添加用户消息到对话中
const userMessage: MessageType = {
id: Date.now().toString(),
content: input,
role: 'user',
content_type: 'text',
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
setError(null);
// 创建临时的bot消息ID用于流式更新
const tempBotMessageId = (Date.now() + 1).toString();
try {
if (isConfigured && isCozeInitialized()) {
// 使用Coze服务发送消息
const cozeService = getCozeService();
// 流式响应处理函数
const handleStreamResponse = (partialText: string) => {
// 更新消息列表,找到临时消息或创建新消息
setMessages(prev => {
// 检查是否已存在临时bot消息
const hasTempMessage = prev.some(msg => msg.id === tempBotMessageId && msg.role === 'assistant');
if (hasTempMessage) {
// 更新已存在的临时消息
return prev.map(msg =>
msg.id === tempBotMessageId && msg.role === 'assistant'
? { ...msg, content: partialText }
: msg
);
} else {
// 创建新的临时bot消息
const tempBotMessage: MessageType = {
id: tempBotMessageId,
content: partialText,
role: 'assistant',
content_type: 'text',
timestamp: new Date(),
};
return [...prev, tempBotMessage];
}
});
};
// 发送消息并传入流式回调
await cozeService.sendMessage(input.trim(), handleStreamResponse);
} else {
// 如果未配置或初始化失败,使用模拟的回复
await new Promise(resolve => setTimeout(resolve, 1000));
const mockBotMessage: MessageType = {
id: (Date.now() + 1).toString(),
content: '您的消息已收到!这是一条智能回复,为您提供专业的文案建议。\n\n根据您的需求我可以为您提供\n- 吸引人的标题创意\n- 产品描述优化\n- 社交媒体文案\n- 广告标语设计\n\n请继续告诉我更多关于您的产品或服务信息我会提供更有针对性的建议',
role: 'assistant',
content_type: 'text',
timestamp: new Date(),
};
setMessages(prev => [...prev, mockBotMessage]);
}
} catch (error) {
console.error('发送消息失败:', error);
// 获取具体的错误信息
const errorMsg = error instanceof Error ? error.message : '未知错误';
const errorMessage: MessageType = {
id: (Date.now() + 2).toString(),
content: `抱歉,处理您的请求时遇到了问题。请稍后再试,或刷新页面重试。`,
role: 'assistant',
content_type: 'text',
timestamp: new Date(),
};
setMessages(prev => [...prev, errorMessage]);
setError(`消息发送失败: ${errorMsg}`);
setShowError(true);
} finally {
setIsLoading(false);
}
};
// 处理错误提示关闭
const handleCloseError = () => {
setShowError(false);
};
// 处理回车键发送消息
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
// 处理新建对话
const handleNewConversation = () => {
if (isConfigured && isCozeInitialized()) {
const cozeService = getCozeService();
cozeService.resetConversation();
}
// 重置消息列表,只保留欢迎消息
setMessages([welcomeMessage]);
console.log('已开始新对话');
};
// 渲染聊天界面
return (
<MuiThemeProvider>
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh', backgroundColor: theme.palette.background.default }}>
{/* 顶部导航 */}
<Paper
elevation={2}
sx={{
backgroundColor: theme.palette.primary.main,
color: 'white',
px: { xs: 2, md: 4 },
py: 2.5,
position: 'relative',
zIndex: 10,
}}
>
<Container maxWidth="xl">
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box>
<Typography variant="h5" component="h1" sx={{ fontWeight: 700, mb: 0.5 }}>
</Typography>
<Typography variant="subtitle2" sx={{ opacity: 0.9, fontSize: '0.9rem' }}>
AI帮你生成吸引人的文案内容
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1.5 }}>
<IconButton
size="small"
color="inherit"
onClick={handleNewConversation}
title="新建对话"
sx={{
backgroundColor: 'rgba(255,255,255,0.15)',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.25)',
},
}}
>
<MessageIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="inherit"
onClick={() => setActiveTab(activeTab === 'chat' ? 'help' : 'chat')}
title={activeTab === 'chat' ? '帮助' : '返回聊天'}
sx={{
backgroundColor: 'rgba(255,255,255,0.15)',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.25)',
},
}}
>
<Help fontSize="small" />
</IconButton>
<IconButton
size="small"
color="inherit"
title="设置"
sx={{
backgroundColor: 'rgba(255,255,255,0.15)',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.25)',
},
}}
>
<Settings fontSize="small" />
</IconButton>
</Box>
</Box>
</Container>
</Paper>
{/* 主内容区域 */}
<Box sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
{/* 侧边栏 - 仅在中等及以上屏幕显示 */}
{!isMobile && <Sidebar />}
{/* 聊天内容区 */}
<Box
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.paper,
position: 'relative',
overflow: 'hidden',
}}
>
{/* 内容切换标签 */}
<Box
sx={{
display: 'flex',
borderBottom: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.default,
px: 2,
}}
>
<Box
sx={{
px: 3,
py: 2,
cursor: 'pointer',
borderBottom: activeTab === 'chat'
? `2px solid ${theme.palette.primary.main}`
: 'none',
color: activeTab === 'chat'
? theme.palette.primary.main
: theme.palette.text.secondary,
fontWeight: activeTab === 'chat' ? 600 : 400,
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}}
onClick={() => setActiveTab('chat')}
>
</Box>
<Box
sx={{
px: 3,
py: 2,
cursor: 'pointer',
borderBottom: activeTab === 'help'
? `2px solid ${theme.palette.primary.main}`
: 'none',
color: activeTab === 'help'
? theme.palette.primary.main
: theme.palette.text.secondary,
fontWeight: activeTab === 'help' ? 600 : 400,
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}}
onClick={() => setActiveTab('help')}
>
使
</Box>
</Box>
{/* 帮助页面内容 */}
{activeTab === 'help' && (
<Box
sx={{
flex: 1,
p: { xs: 2, md: 4 },
overflow: 'auto',
}}
>
<Typography variant="h5" component="h2" sx={{ mb: 3, fontWeight: 600 }}>
使
</Typography>
<Paper elevation={1} sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" sx={{ mb: 2, color: theme.palette.primary.main }}>
</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
1.
</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
2. 广
</Typography>
<Typography variant="body1">
3.
</Typography>
</Paper>
<Paper elevation={1} sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" sx={{ mb: 2, color: theme.palette.primary.main }}>
</Typography>
<Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 500 }}>
</Typography>
<Typography variant="body2" sx={{ mb: 2, ml: 4, color: theme.palette.text.secondary }}>
广
</Typography>
<Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 500 }}>
</Typography>
<Typography variant="body2" sx={{ mb: 2, ml: 4, color: theme.palette.text.secondary }}>
</Typography>
</Paper>
</Box>
)}
{/* 聊天消息区域 */}
{activeTab === 'chat' && (
<>
<Box
sx={{
flex: 1,
overflow: 'auto',
p: { xs: 2, md: 4 },
display: 'flex',
flexDirection: 'column',
gap: 2,
backgroundColor: (theme) => theme.palette.mode === 'dark'
? theme.palette.background.default
: '#fafafa',
}}
ref={chatContainerRef}
>
{messages.map((message, index) => (
<Message
key={message.id}
message={message}
isLast={index === messages.length - 1 && !isLoading}
/>
))}
{isLoading && (
<Box display="flex" alignItems="flex-end" gap={1.5} maxWidth={isMobile ? "85%" : "70%"}>
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: theme.palette.primary.main,
boxShadow: theme.shadows[1],
}}
>
🤖
</Avatar>
<Paper
elevation={1}
sx={{
padding: 2,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
borderRadius: '18px 18px 18px 4px',
minWidth: '120px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Box sx={{ display: 'flex', gap: 1 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: theme.palette.primary.main,
animation: 'pulse 1.4s infinite ease-in-out',
}}
/>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: theme.palette.primary.main,
animation: 'pulse 1.4s infinite ease-in-out 0.2s',
}}
/>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: theme.palette.primary.main,
animation: 'pulse 1.4s infinite ease-in-out 0.4s',
}}
/>
</Box>
</Paper>
</Box>
)}
</Box>
{/* 输入区域 */}
<MessageInput
value={input}
onChange={setInput}
onSend={handleSendMessage}
onKeyDown={handleKeyPress}
disabled={isLoading}
/>
</>
)}
</Box>
</Box>
{/* 错误提示 */}
<Snackbar
open={showError}
autoHideDuration={6000}
onClose={handleCloseError}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={handleCloseError}
severity="error"
variant="filled"
sx={{ width: '100%' }}
>
{error}
</Alert>
</Snackbar>
</Box>
</MuiThemeProvider>
);
};
export default ChatInterface;