Files
ai-novel/src/pages/ConversationsPage.tsx
2026-04-16 21:32:21 +08:00

845 lines
29 KiB
TypeScript
Raw 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.
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;