845 lines
29 KiB
TypeScript
845 lines
29 KiB
TypeScript
|
|
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:message、im: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;
|