first commit
This commit is contained in:
495
app/page.tsx
Normal file
495
app/page.tsx
Normal file
@ -0,0 +1,495 @@
|
||||
"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;
|
||||
Reference in New Issue
Block a user