Files
ai-novel/src/pages/ConversationsPage.tsx

845 lines
29 KiB
TypeScript
Raw Normal View History

2026-04-16 21:32:21 +08:00
import React, { useState, useEffect } from 'react';
import {
Layout,
List,
Input,
Button,
Avatar,
Typography,
Space,
Modal,
Form,
Select,
Tag,
message,
Dropdown,
Popconfirm,
Alert
} from 'antd';
import {
SendOutlined,
PlusOutlined,
RobotOutlined,
UserOutlined,
SettingOutlined,
MessageOutlined,
DeleteOutlined,
EditOutlined,
PlusSquareOutlined
} from '@ant-design/icons';
const { Sider, Content } = Layout;
const { Title, Text } = Typography;
const { TextArea } = Input;
interface Message {
id: string;
content: string;
role: 'user' | 'assistant';
timestamp: string;
}
interface Conversation {
id: string;
title: string;
botName: string;
botType: 'feishu' | 'other';
lastMessage: string;
timestamp: string;
messages: Message[];
}
interface BotConfig {
id: string;
name: string;
type: 'feishu' | 'other';
appId: string;
appSecret: string;
description: string;
}
const ConversationsPage: React.FC = () => {
const [conversations, setConversations] = useState<Conversation[]>([]);
const [selectedConversation, setSelectedConversation] = useState<Conversation | null>(null);
const [messageInput, setMessageInput] = useState('');
const [botConfigs, setBotConfigs] = useState<BotConfig[]>([]);
const [showBotModal, setShowBotModal] = useState(false);
const [showConversationModal, setShowConversationModal] = useState(false);
const [editingBot, setEditingBot] = useState<BotConfig | null>(null);
const [form] = Form.useForm();
const [conversationForm] = Form.useForm();
useEffect(() => {
loadData();
}, []);
const loadData = () => {
// 加载飞书机器人配置
const savedBots = localStorage.getItem('botConfigs');
if (savedBots) {
setBotConfigs(JSON.parse(savedBots));
} else {
// 默认配置示例
const defaultBots: BotConfig[] = [
{
id: '1',
name: '助手机器人',
type: 'feishu',
appId: 'cli_xxxxxxxxxxxxx',
appSecret: 'xxxxxxxxxxxxx',
description: '用于日常事务处理和提醒'
}
];
setBotConfigs(defaultBots);
localStorage.setItem('botConfigs', JSON.stringify(defaultBots));
}
// 加载会话列表
const savedConversations = localStorage.getItem('conversations');
if (savedConversations) {
setConversations(JSON.parse(savedConversations));
} else {
// 默认会话示例
const defaultConversations: Conversation[] = [
{
id: '1',
title: '欢迎使用飞书机器人',
botName: '助手机器人',
botType: 'feishu',
lastMessage: '你好!我是飞书助手机器人,有什么可以帮助您的吗?',
timestamp: new Date().toISOString(),
messages: [
{
id: '1',
content: '你好!我是飞书助手机器人,有什么可以帮助您的吗?',
role: 'assistant',
timestamp: new Date().toISOString()
}
]
}
];
setConversations(defaultConversations);
localStorage.setItem('conversations', JSON.stringify(defaultConversations));
}
};
const handleSendMessage = async () => {
if (!messageInput.trim() || !selectedConversation) return;
const newMessage: Message = {
id: Date.now().toString(),
content: messageInput,
role: 'user',
timestamp: new Date().toISOString()
};
const updatedConversation = {
...selectedConversation,
messages: [...selectedConversation.messages, newMessage],
lastMessage: messageInput,
timestamp: new Date().toISOString()
};
setSelectedConversation(updatedConversation);
setConversations(conversations.map(conv =>
conv.id === selectedConversation.id ? updatedConversation : conv
));
// 保存到本地存储
localStorage.setItem('conversations', JSON.stringify(
conversations.map(conv => conv.id === selectedConversation.id ? updatedConversation : conv)
));
setMessageInput('');
// 模拟飞书应用回复
setTimeout(() => {
const botReply: Message = {
id: (Date.now() + 1).toString(),
content: `收到您的消息:"${messageInput}"。这是一个模拟的飞书应用回复。在实际使用中这里会调用飞书自建应用API来发送消息。`,
role: 'assistant',
timestamp: new Date().toISOString()
};
const updatedWithReply = {
...updatedConversation,
messages: [...updatedConversation.messages, botReply],
lastMessage: botReply.content,
timestamp: new Date().toISOString()
};
setSelectedConversation(updatedWithReply);
setConversations(conversations.map(conv =>
conv.id === selectedConversation.id ? updatedWithReply : conv
));
localStorage.setItem('conversations', JSON.stringify(
conversations.map(conv => conv.id === selectedConversation.id ? updatedWithReply : conv)
));
}, 1000);
};
const handleSubmitConversation = () => {
conversationForm.validateFields().then(values => {
const selectedBot = botConfigs.find(bot => bot.id === values.botId);
if (!selectedBot) return;
const newConversation: Conversation = {
id: Date.now().toString(),
title: values.title,
botName: selectedBot.name,
botType: selectedBot.type,
lastMessage: '新会话已创建',
timestamp: new Date().toISOString(),
messages: []
};
setConversations([newConversation, ...conversations]);
localStorage.setItem('conversations', JSON.stringify([newConversation, ...conversations]));
setShowConversationModal(false);
conversationForm.resetFields();
setSelectedConversation(newConversation);
message.success('会话创建成功');
});
};
const handleAddBot = () => {
setEditingBot(null);
form.resetFields();
setShowBotModal(true);
};
const handleEditBot = (bot: BotConfig) => {
setEditingBot(bot);
form.setFieldsValue(bot);
setShowBotModal(true);
};
const handleDeleteBot = (botId: string) => {
const updatedBots = botConfigs.filter(bot => bot.id !== botId);
setBotConfigs(updatedBots);
localStorage.setItem('botConfigs', JSON.stringify(updatedBots));
message.success('机器人配置已删除');
};
const handleSubmitBot = () => {
form.validateFields().then(values => {
if (editingBot) {
// 编辑模式
const updatedBot: BotConfig = {
...editingBot,
name: values.name,
type: values.type,
appId: values.appId,
appSecret: values.appSecret,
description: values.description
};
const updatedBots = botConfigs.map(bot =>
bot.id === editingBot.id ? updatedBot : bot
);
setBotConfigs(updatedBots);
localStorage.setItem('botConfigs', JSON.stringify(updatedBots));
message.success('飞书应用配置已更新');
} else {
// 新增模式
const newBot: BotConfig = {
id: Date.now().toString(),
name: values.name,
type: values.type,
appId: values.appId,
appSecret: values.appSecret,
description: values.description
};
setBotConfigs([...botConfigs, newBot]);
localStorage.setItem('botConfigs', JSON.stringify([...botConfigs, newBot]));
message.success('飞书应用配置已添加');
}
setShowBotModal(false);
form.resetFields();
setEditingBot(null);
});
};
const handleDeleteConversation = (conversationId: string) => {
const updatedConversations = conversations.filter(conv => conv.id !== conversationId);
setConversations(updatedConversations);
localStorage.setItem('conversations', JSON.stringify(updatedConversations));
if (selectedConversation?.id === conversationId) {
setSelectedConversation(null);
}
message.success('会话已删除');
};
const getBotItems = () => {
return [
{
key: 'add',
label: (
<div onClick={() => handleAddBot()}>
<PlusSquareOutlined style={{ marginRight: '8px' }} />
</div>
)
},
{
type: 'divider' as const,
},
...botConfigs.map(bot => ({
key: bot.id,
label: (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>
<RobotOutlined style={{ marginRight: '8px', color: '#1890ff' }} />
{bot.name}
<Tag color={bot.type === 'feishu' ? 'blue' : 'green'} style={{ marginLeft: '8px' }}>
{bot.type === 'feishu' ? '飞书' : '其他'}
</Tag>
</span>
<Space size={4}>
<EditOutlined
style={{ color: '#52c41a', cursor: 'pointer' }}
onClick={(e) => { e.stopPropagation(); handleEditBot(bot); }}
/>
<Popconfirm
title="确认删除"
description="确定要删除这个飞书应用配置吗?"
onConfirm={(e) => {
e?.stopPropagation();
handleDeleteBot(bot.id);
}}
okText="确定"
cancelText="取消"
>
<DeleteOutlined
style={{ color: '#ff4d4f', cursor: 'pointer' }}
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>
</Space>
</div>
)
}))
];
};
return (
<div style={{ height: 'calc(100vh - 184px)' }}>
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Title level={2} style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '12px' }}>
<MessageOutlined style={{ color: '#1890ff' }} />
</Title>
<Text type="secondary" style={{ marginTop: '8px', display: 'block' }}>
AI助手完成各种任务
</Text>
</div>
<Dropdown
menu={{ items: getBotItems() }}
trigger={['click']}
placement="bottomRight"
>
<Button icon={<SettingOutlined />}>
({botConfigs.length})
</Button>
</Dropdown>
</div>
<Layout style={{
background: 'white',
borderRadius: '12px',
height: 'calc(100% - 80px)',
overflow: 'hidden'
}}>
{/* 左侧会话列表 */}
<Sider
width={320}
style={{
background: '#fafafa',
borderRight: '1px solid #f0f0f0',
height: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
<Input.Search
placeholder="搜索会话..."
style={{ width: '100%' }}
/>
</div>
{/* 飞书应用列表 */}
<div style={{ padding: '12px 16px', borderBottom: '1px solid #f0f0f0' }}>
<div style={{ fontSize: '12px', color: '#999', marginBottom: '8px', fontWeight: 'bold' }}>
</div>
{botConfigs.length > 0 ? (
<Space direction="vertical" style={{ width: '100%' }} size={8}>
{botConfigs.map(bot => {
const hasConversation = conversations.some(conv => conv.botName === bot.name);
return (
<div
key={bot.id}
onClick={() => {
if (hasConversation) {
const existingConv = conversations.find(conv => conv.botName === bot.name);
if (existingConv) {
setSelectedConversation(existingConv);
}
} else {
// 创建新会话
const newConversation: Conversation = {
id: Date.now().toString(),
title: bot.name,
botName: bot.name,
botType: bot.type,
lastMessage: '新会话已创建',
timestamp: new Date().toISOString(),
messages: []
};
setConversations([newConversation, ...conversations]);
localStorage.setItem('conversations', JSON.stringify([newConversation, ...conversations]));
setSelectedConversation(newConversation);
}
}}
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '10px 12px',
background: selectedConversation?.botName === bot.name ? '#e6f7ff' : 'white',
borderRadius: '8px',
cursor: 'pointer',
border: '1px solid #f0f0f0',
transition: 'all 0.3s'
}}
onMouseEnter={(e) => {
if (selectedConversation?.botName !== bot.name) {
e.currentTarget.style.background = '#f5f5f5';
e.currentTarget.style.borderColor = '#d9d9d9';
}
}}
onMouseLeave={(e) => {
if (selectedConversation?.botName !== bot.name) {
e.currentTarget.style.background = 'white';
e.currentTarget.style.borderColor = '#f0f0f0';
}
}}
>
<Avatar
icon={<RobotOutlined />}
style={{ backgroundColor: '#1890ff' }}
/>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Text strong style={{ fontSize: '14px' }}>
{bot.name}
</Text>
<Tag color={bot.type === 'feishu' ? 'blue' : 'green'}>
{bot.type === 'feishu' ? '飞书' : '其他'}
</Tag>
</div>
<Text type="secondary" style={{ fontSize: '11px' }}>
{hasConversation ? '点击继续对话' : '点击开始对话'}
</Text>
</div>
</div>
);
})}
</Space>
) : (
<div style={{ textAlign: 'center', padding: '20px 0', color: '#999' }}>
<Text type="secondary"></Text>
<div style={{ marginTop: '8px' }}>
<Button type="link" size="small" onClick={handleAddBot}>
</Button>
</div>
</div>
)}
</div>
{/* 会话列表 */}
<div style={{ padding: '12px 16px 0', flex: 1, overflowY: 'auto' }}>
<div style={{ fontSize: '12px', color: '#999', marginBottom: '8px', fontWeight: 'bold' }}>
</div>
<List
dataSource={conversations}
renderItem={(conversation) => (
<List.Item
key={conversation.id}
onClick={() => setSelectedConversation(conversation)}
style={{
padding: '12px',
cursor: 'pointer',
background: selectedConversation?.id === conversation.id ? '#e6f7ff' : 'transparent',
borderRadius: '8px',
marginBottom: '8px',
border: '1px solid #f0f0f0',
transition: 'all 0.3s'
}}
onMouseEnter={(e) => {
if (selectedConversation?.id !== conversation.id) {
e.currentTarget.style.background = '#f5f5f5';
e.currentTarget.style.borderColor = '#d9d9d9';
}
}}
onMouseLeave={(e) => {
if (selectedConversation?.id !== conversation.id) {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.borderColor = '#f0f0f0';
}
}}
>
<List.Item.Meta
avatar={
<Avatar
icon={<RobotOutlined />}
style={{ backgroundColor: '#1890ff' }}
/>
}
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text strong ellipsis style={{ maxWidth: '140px' }}>
{conversation.title}
</Text>
<Tag color={conversation.botType === 'feishu' ? 'blue' : 'green'}>
{conversation.botType === 'feishu' ? '飞书' : '其他'}
</Tag>
</div>
}
description={
<div>
<Text ellipsis style={{ fontSize: '11px', color: '#666' }}>
{conversation.lastMessage}
</Text>
<Text style={{ fontSize: '10px', color: '#999', display: 'block', marginTop: '2px' }}>
{new Date(conversation.timestamp).toLocaleString()}
</Text>
</div>
}
/>
</List.Item>
)}
/>
</div>
</Sider>
{/* 右侧会话内容 */}
<Content style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{selectedConversation ? (
<>
{/* 会话头部 */}
<div style={{
padding: '16px 24px',
borderBottom: '1px solid #f0f0f0',
background: '#fafafa'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<Avatar icon={<RobotOutlined />} style={{ backgroundColor: '#1890ff' }} />
<div>
<Title level={4} style={{ margin: 0 }}>
{selectedConversation.title}
</Title>
<Text type="secondary" style={{ fontSize: '12px' }}>
{selectedConversation.botName}
</Text>
</div>
</Space>
<Popconfirm
title="确认删除"
description="确定要删除这个会话吗?"
onConfirm={() => handleDeleteConversation(selectedConversation.id)}
okText="确定"
cancelText="取消"
>
<Button danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</div>
</div>
{/* 消息列表 */}
<div style={{
flex: 1,
padding: '24px',
overflowY: 'auto',
background: 'white'
}}>
{selectedConversation.messages.length === 0 ? (
<div style={{
textAlign: 'center',
padding: '100px 0',
color: '#999'
}}>
<MessageOutlined style={{ fontSize: '64px', color: '#d9d9d9', marginBottom: '16px' }} />
<div></div>
</div>
) : (
<Space direction="vertical" style={{ width: '100%' }} size={16}>
{selectedConversation.messages.map((message) => (
<div
key={message.id}
style={{
display: 'flex',
justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start',
marginBottom: '16px'
}}
>
<div style={{
maxWidth: '70%',
display: 'flex',
gap: '8px',
alignItems: 'flex-start'
}}>
{message.role === 'assistant' && (
<Avatar icon={<RobotOutlined />} style={{ backgroundColor: '#1890ff' }} />
)}
<div>
{message.role === 'assistant' && (
<Text type="secondary" style={{ fontSize: '12px', marginLeft: '8px' }}>
{selectedConversation.botName}
</Text>
)}
<div style={{
background: message.role === 'user' ? '#1890ff' : '#f5f5f5',
color: message.role === 'user' ? 'white' : 'black',
padding: '12px 16px',
borderRadius: '8px',
marginTop: '4px',
wordBreak: 'break-word'
}}>
{message.content}
</div>
<Text type="secondary" style={{ fontSize: '11px', marginTop: '4px', display: 'block' }}>
{new Date(message.timestamp).toLocaleString()}
</Text>
</div>
{message.role === 'user' && (
<Avatar icon={<UserOutlined />} style={{ backgroundColor: '#52c41a' }} />
)}
</div>
</div>
))}
</Space>
)}
</div>
{/* 消息输入区 */}
<div style={{
padding: '16px 24px',
borderTop: '1px solid #f0f0f0',
background: '#fafafa'
}}>
<Space.Compact style={{ width: '100%' }}>
<TextArea
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
placeholder="输入消息按Enter发送Shift+Enter换行"
autoSize={{ minRows: 1, maxRows: 4 }}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSendMessage}
style={{ height: 'auto' }}
>
</Button>
</Space.Compact>
</div>
</>
) : (
<div style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'white'
}}>
<div style={{ textAlign: 'center', color: '#999' }}>
<MessageOutlined style={{ fontSize: '64px', color: '#d9d9d9', marginBottom: '16px' }} />
<div style={{ fontSize: '16px', marginBottom: '16px' }}>
</div>
{botConfigs.length === 0 && (
<div>
<div style={{ marginBottom: '16px' }}></div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddBot}
>
</Button>
</div>
)}
</div>
</div>
)}
</Content>
</Layout>
{/* 飞书应用配置弹窗 */}
<Modal
title={
<Space>
<RobotOutlined style={{ color: '#1890ff' }} />
{editingBot ? '编辑飞书应用配置' : '添加飞书自建应用'}
</Space>
}
open={showBotModal}
onOk={handleSubmitBot}
onCancel={() => {
setShowBotModal(false);
form.resetFields();
setEditingBot(null);
}}
width={700}
okText="确定"
cancelText="取消"
>
<Alert
message="飞书自建应用配置说明"
description={
<div>
<p>1. </p>
<p>2. App ID App Secret</p>
<p>3. im:messageim:resource </p>
</div>
}
type="info"
showIcon
style={{ marginBottom: '16px' }}
/>
<Form
form={form}
layout="vertical"
style={{ marginTop: '24px' }}
initialValues={{
type: 'feishu'
}}
>
<Form.Item
name="name"
label="应用名称"
rules={[{ required: true, message: '请输入应用名称' }]}
>
<Input placeholder="例如AI助手" />
</Form.Item>
<Form.Item
name="type"
label="应用类型"
rules={[{ required: true, message: '请选择应用类型' }]}
>
<Select>
<Select.Option value="feishu"></Select.Option>
<Select.Option value="other"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="appId"
label="App ID"
rules={[{ required: true, message: '请输入 App ID' }]}
>
<Input
placeholder="cli_xxxxxxxxxxxxx"
prefix={<span style={{ color: '#999' }}>App ID:</span>}
/>
</Form.Item>
<Form.Item
name="appSecret"
label="App Secret"
rules={[{ required: true, message: '请输入 App Secret' }]}
>
<Input.Password
placeholder="xxxxxxxxxxxxx"
prefix={<span style={{ color: '#999' }}>Secret:</span>}
/>
</Form.Item>
<Form.Item
name="description"
label="应用描述"
>
<TextArea
placeholder="描述应用的用途和功能"
rows={3}
showCount
maxLength={200}
/>
</Form.Item>
</Form>
</Modal>
{/* 新建会话弹窗 */}
<Modal
title={
<Space>
<MessageOutlined style={{ color: '#1890ff' }} />
</Space>
}
open={showConversationModal}
onOk={handleSubmitConversation}
onCancel={() => {
setShowConversationModal(false);
conversationForm.resetFields();
}}
okText="创建"
cancelText="取消"
>
<Form
form={conversationForm}
layout="vertical"
style={{ marginTop: '24px' }}
>
<Form.Item
name="title"
label="会话标题"
rules={[{ required: true, message: '请输入会话标题' }]}
>
<Input placeholder="例如:工作安排助手" />
</Form.Item>
<Form.Item
name="botId"
label="选择飞书应用"
rules={[{ required: true, message: '请选择飞书应用' }]}
>
<Select placeholder="选择要使用的飞书应用">
{botConfigs.map(bot => (
<Select.Option key={bot.id} value={bot.id}>
<Space>
<RobotOutlined />
{bot.name}
<Tag color={bot.type === 'feishu' ? 'blue' : 'green'}>
{bot.type === 'feishu' ? '飞书' : '其他'}
</Tag>
</Space>
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default ConversationsPage;