Files
leaudit-platform-frontend/app/components/dify-chat/sidebar.tsx
T
2025-12-08 21:36:03 +08:00

451 lines
17 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 {
DeleteOutlined,
EditOutlined,
ExclamationCircleOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
MessageOutlined,
MoreOutlined,
PlusOutlined,
SearchOutlined,
} from '@ant-design/icons';
import { Button, Dropdown, Input, Layout, Menu, Modal, Tooltip, message, theme, Select } from 'antd';
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
import type { ChatApp } from '~/api/dify-chat-apps/types';
import type { ConversationItem } from '~/api/dify-chat';
import { deleteConversation, renameConversation } from '~/api/dify-chat';
import '../../styles/components/chat-with-llm/sidebar.css';
const { Sider } = Layout;
interface ChatSidebarProps {
collapsed: boolean;
onToggle: () => void;
conversations: ConversationItem[];
currentConversationId: string;
onConversationSelect: (conversationId: string) => void;
onNewConversation: () => void;
onConversationDeleted?: (conversationId: string) => void;
// 对话应用相关属性
chatApps: ChatApp[];
loadingChatApps: boolean;
currentChatApp: ChatApp | null;
onChatAppChange: (appId: string) => void;
onConversationRenamed?: (conversationId: string, newName: string) => void;
}
// 暴露给父组件的方法接口
export interface ChatSidebarRef {
autoRename: (conversationId: string) => Promise<void>;
}
/**
* 聊天侧边栏组件
*/
const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
collapsed,
onToggle,
chatApps,
loadingChatApps,
currentChatApp,
onChatAppChange,
conversations,
currentConversationId,
onConversationSelect,
onNewConversation,
onConversationDeleted,
onConversationRenamed,
}, ref) => {
const [searchValue, setSearchValue] = useState('');
const [renameModalVisible, setRenameModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [renamingConversation, setRenamingConversation] = useState<ConversationItem | null>(null);
const [deletingConversation, setDeletingConversation] = useState<ConversationItem | null>(null);
const [newName, setNewName] = useState('');
const [renameLoading, setRenameLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();
// 去重:防止后端返回重复的应用数据
const uniqueChatApps = useMemo(() => {
const appMap = new Map<string, ChatApp>();
chatApps.forEach(app => {
// 如果已经存在相同ID的应用,保留第一个(或检查is_default属性)
if (!appMap.has(app.app_id)) {
appMap.set(app.app_id, app);
} else {
// 如果重复的是默认应用,保留默认版本
const existingApp = appMap.get(app.app_id)!;
if (app.is_default && !existingApp.is_default) {
appMap.set(app.app_id, app);
}
}
});
const unique = Array.from(appMap.values());
console.log('[Sidebar] 应用去重:', {
originalCount: chatApps.length,
uniqueCount: unique.length,
removed: chatApps.length - unique.length
});
return unique;
}, [chatApps]);
// 过滤会话列表
const filteredConversations = conversations.filter(conv =>
conv.name.toLowerCase().includes(searchValue.toLowerCase())
);
// 处理重命名
const handleRename = (conv: ConversationItem) => {
setRenamingConversation(conv);
setNewName(conv.name);
setRenameModalVisible(true);
};
// 处理删除会话 - 显示确认Modal
const handleDeleteClick = (conv: ConversationItem) => {
setDeletingConversation(conv);
setDeleteModalVisible(true);
};
// 确认删除会话
const handleDeleteConfirm = async () => {
if (!deletingConversation) return;
setDeleteLoading(true);
try {
// console.log('🗑️ 开始删除会话:', deletingConversation.id);
// 调用API删除服务器端的会话
const response = await deleteConversation(deletingConversation.id);
// console.log('✅ 服务器端会话删除响应:', response);
// 检查响应是否成功
if (response && (response as any).result === 'success') {
// console.log('✅ 服务器端会话删除成功');
message.success('会话删除成功');
setDeleteModalVisible(false);
// 通知父组件会话已删除
onConversationDeleted?.(deletingConversation.id);
// console.log('✅ 会话删除完成:', deletingConversation.id);
} else {
throw new Error((response as any)?.error || '删除会话失败');
}
} catch (error) {
console.error('❌ 删除会话失败:', error);
message.error(`删除会话失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setDeleteLoading(false);
}
};
// 取消删除
const handleDeleteCancel = () => {
setDeleteModalVisible(false);
setDeletingConversation(null);
setDeleteLoading(false);
};
// 确认重命名
const handleRenameConfirm = async () => {
if (!renamingConversation || !newName.trim()) {
message.error('请输入有效的会话名称');
return;
}
if (newName.trim() === renamingConversation.name) {
setRenameModalVisible(false);
return;
}
setRenameLoading(true);
try {
// console.log('✏️ 开始重命名会话:', { conversationId: renamingConversation.id, newName: newName.trim() });
// 调用API重命名服务器端的会话
const response = await renameConversation(renamingConversation.id, newName.trim(), false);
// console.log('✅ 服务器端会话重命名响应:', response);
// 检查响应是否成功
if (response && (response as any).name) {
// console.log('✅ 服务器端会话重命名成功');
message.success('重命名成功');
setRenameModalVisible(false);
// 通知父组件会话已重命名
onConversationRenamed?.(renamingConversation.id, (response as any).name);
// console.log('✅ 会话重命名完成:', renamingConversation.id, '->', (response as any).name);
} else {
throw new Error((response as any)?.error || '重命名会话失败');
}
} catch (error) {
console.error('❌ 重命名会话失败:', error);
message.error(`重命名失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setRenameLoading(false);
}
};
// 取消重命名
const handleRenameCancel = () => {
setRenameModalVisible(false);
setRenamingConversation(null);
setNewName('');
setRenameLoading(false);
};
// 生成菜单项
const menuItems = filteredConversations.map(conv => ({
key: conv.id,
icon: <MessageOutlined />,
label: (
<div className="flex items-center justify-between group">
<span className="truncate flex-1" title={conv.name}>
{conv.name}
</span>
{!collapsed && (
<Dropdown
menu={{
items: [
{
key: 'rename',
icon: <EditOutlined />,
label: '重命名',
onClick: () => handleRename(conv),
},
{
key: 'delete',
icon: <DeleteOutlined />,
label: '删除',
danger: true,
onClick: () => handleDeleteClick(conv),
},
],
}}
trigger={['click']}
placement="bottomRight"
>
<Button
type="text"
size="small"
icon={<MoreOutlined />}
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
// style={{ backgroundColor: '#00684A' }}
/>
</Dropdown>
)}
</div>
),
}));
useImperativeHandle(ref, () => ({
autoRename: async (conversationId: string) => {
try {
// console.log('🏷️ 开始自动重命名会话为"新对话":', conversationId);
// 调用API将会话重命名为固定的"新对话"
const response = await renameConversation(conversationId, '新对话', false);
// console.log('✅ 服务器端会话重命名响应:', response);
// 检查响应是否成功
if (response && (response as any).name) {
// console.log('✅ 服务器端会话重命名成功');
// 通知父组件会话已重命名
onConversationRenamed?.(conversationId, (response as any).name);
// console.log('✅ 会话重命名完成:', conversationId, '->', (response as any).name);
} else {
throw new Error((response as any)?.error || '重命名会话失败');
}
} catch (error) {
console.error('❌ 重命名会话失败:', error);
// 重命名失败时不显示错误消息,避免打扰用户
// console.warn('⚠️ 重命名失败,会话将保持默认名称');
}
},
}));
return (
<Sider
trigger={null}
collapsible
collapsed={collapsed}
width={280}
style={{
background: colorBgContainer,
borderRight: '1px solid #f0f0f0',
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
{/* 侧边栏头部 - 固定在顶部 */}
<div className="p-4 border-b border-gray-100 flex-shrink-0">
<div className="flex items-center justify-between mb-3">
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={onToggle}
style={{
fontSize: '16px',
width: 32,
height: 32,
color: 'rgb(0, 104, 74)',
}}
/>
{!collapsed && (
<Tooltip title="新建对话">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={onNewConversation}
size="small"
>
</Button>
</Tooltip>
)}
</div>
{/* 搜索框 */}
{/* 对话应用选择器 */}
{!collapsed && (
<div className="mb-3">
<Select
value={currentChatApp?.app_id}
onChange={onChatAppChange}
loading={loadingChatApps}
className="w-full"
placeholder="选择对话应用"
size="small"
>
{uniqueChatApps.map(app => (
<Select.Option key={app.app_id} value={app.app_id}>
{app.app_name}
</Select.Option>
))}
</Select>
</div>
)}
{!collapsed && (
<Input
placeholder="搜索对话..."
prefix={<SearchOutlined />}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
allowClear
/>
)}
</div>
{/* 会话列表 - 可滚动区域 */}
<div className="flex-1 overflow-hidden">
<div
className="h-full overflow-y-auto"
style={{
scrollbarWidth: 'thin',
scrollbarColor: '#c1c1c1 #f1f1f1',
}}
>
{!collapsed && filteredConversations.length === 0 && searchValue && (
<div className="p-4 text-center text-gray-500">
<MessageOutlined className="text-2xl mb-2" />
<p></p>
</div>
)}
{!collapsed && conversations.length === 0 && !searchValue && (
<div className="p-4 text-center text-gray-500">
<MessageOutlined className="text-2xl mb-2" />
<p></p>
<Button
type="link"
onClick={onNewConversation}
className="mt-2"
>
</Button>
</div>
)}
<Menu
mode="inline"
selectedKeys={[currentConversationId]}
items={menuItems}
onClick={({ key }) => onConversationSelect(key)}
style={{
border: 'none',
background: 'transparent',
}}
className="chat-sidebar-menu"
/>
</div>
</div>
{/* 侧边栏底部 - 固定在底部 */}
{!collapsed && conversations.length > 0 && (
<div className="sidebar-footer">
<div className="stats-text">
{conversations.length}
</div>
</div>
)}
{/* 重命名Modal */}
<Modal
title="重命名会话"
open={renameModalVisible}
onOk={handleRenameConfirm}
onCancel={handleRenameCancel}
confirmLoading={renameLoading}
okText="确定"
cancelText="取消"
destroyOnClose
>
<div className="py-4">
<Input
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="请输入新的会话名称"
maxLength={10}
showCount
onPressEnter={handleRenameConfirm}
autoFocus
/>
</div>
</Modal>
{/* 删除确认Modal */}
<Modal
title={
<div className="flex items-center">
<ExclamationCircleOutlined className="text-red-500 mr-2" />
</div>
}
open={deleteModalVisible}
onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel}
confirmLoading={deleteLoading}
okText="删除"
cancelText="取消"
okType="danger"
destroyOnClose
>
<div className="py-4">
<p> <strong>"{deletingConversation?.name}"</strong> </p>
<p className="text-gray-500 text-sm mt-2"></p>
</div>
</Modal>
</Sider>
);
});
export default ChatSidebar;