基于 shiy-temp分支修改
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
import React, { useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Button, Layout, Menu, theme, Input, Tooltip, Dropdown, Modal, message } from 'antd';
|
||||
import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MessageOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
MoreOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ConversationItem } from '../../types/dify_chat';
|
||||
import { deleteConversation, renameConversation } from '../../services/api.client';
|
||||
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;
|
||||
onConversationRenamed?: (conversationId: string, newName: string) => void;
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法接口
|
||||
export interface ChatSidebarRef {
|
||||
autoRename: (conversationId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天侧边栏组件
|
||||
*/
|
||||
const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
|
||||
collapsed,
|
||||
onToggle,
|
||||
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 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()}
|
||||
/>
|
||||
</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 && (
|
||||
<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 && (
|
||||
<div className="p-4 border-t border-gray-100 flex-shrink-0">
|
||||
<div className="text-xs text-gray-500 text-center">
|
||||
共 {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;
|
||||
Reference in New Issue
Block a user