496 lines
18 KiB
TypeScript
496 lines
18 KiB
TypeScript
"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. 你的目标用户(根据你从业经验描绘的目标用户)',
|
||
sender: 'bot',
|
||
timestamp: new Date(Date.now() - 60000),
|
||
};
|
||
|
||
const ChatInterface: React.FC = () => {
|
||
// 状态管理
|
||
const [messages, setMessages] = useState<MessageType[]>([welcomeMessage]);
|
||
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,
|
||
sender: 'user',
|
||
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.sender === 'bot');
|
||
|
||
if (hasTempMessage) {
|
||
// 更新已存在的临时消息
|
||
return prev.map(msg =>
|
||
msg.id === tempBotMessageId && msg.sender === 'bot'
|
||
? { ...msg, content: partialText }
|
||
: msg
|
||
);
|
||
} else {
|
||
// 创建新的临时bot消息
|
||
const tempBotMessage: MessageType = {
|
||
id: tempBotMessageId,
|
||
content: partialText,
|
||
sender: 'bot',
|
||
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请继续告诉我更多关于您的产品或服务信息,我会提供更有针对性的建议!',
|
||
sender: 'bot',
|
||
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: `抱歉,处理您的请求时遇到了问题。请稍后再试,或刷新页面重试。`,
|
||
sender: 'bot',
|
||
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;
|