Files
AIclinicalresearch/frontend-v2/src/shared/components/Chat/ConversationList.tsx

182 lines
4.5 KiB
TypeScript

/**
* ConversationList - 会话列表组件
*
* 现代感设计的会话列表,支持:
* - 分组显示(今天、昨天、更早)
* - 新建会话
* - 会话切换
* - 会话删除
* - 智能体图标
*/
import React, { useMemo } from 'react';
import { Button, Typography, Dropdown, Empty } from 'antd';
import {
PlusOutlined,
MessageOutlined,
DeleteOutlined,
MoreOutlined,
RobotOutlined,
} from '@ant-design/icons';
import type { Conversation, ConversationGroup } from './hooks/useConversations';
import './styles/conversation-list.css';
const { Text } = Typography;
/**
* ConversationList Props
*/
export interface ConversationListProps {
/** 分组会话列表 */
groups: ConversationGroup[];
/** 当前会话 ID */
currentId: string | null;
/** 会话点击回调 */
onSelect: (id: string) => void;
/** 新建会话回调 */
onNew: () => void;
/** 删除会话回调 */
onDelete?: (id: string) => void;
/** 标题 */
title?: string;
/** Logo */
logo?: React.ReactNode;
/** 自定义类名 */
className?: string;
/** 是否加载中 */
loading?: boolean;
}
/**
* 会话列表组件
*/
export const ConversationList: React.FC<ConversationListProps> = ({
groups,
currentId,
onSelect,
onNew,
onDelete,
title = 'AI 助手',
logo,
className = '',
loading = false,
}) => {
// 会话项菜单
const getMenuItems = (conv: Conversation) => [
{
key: 'delete',
icon: <DeleteOutlined />,
label: '删除对话',
danger: true,
onClick: () => onDelete?.(conv.id),
},
];
// 是否为空
const isEmpty = useMemo(() => {
return groups.every(g => g.conversations.length === 0);
}, [groups]);
return (
<div className={`conversation-list ${className}`}>
{/* 头部 */}
<div className="conversation-list-header">
<div className="conversation-list-logo">
{logo || <RobotOutlined className="default-logo" />}
<span className="conversation-list-title">{title}</span>
</div>
</div>
{/* 新建会话按钮 */}
<div className="conversation-list-new">
<Button
type="default"
icon={<PlusOutlined />}
onClick={onNew}
block
className="new-conversation-btn"
>
</Button>
</div>
{/* 会话列表 */}
<div className="conversation-list-content">
{isEmpty ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无对话"
className="conversation-empty"
/>
) : (
groups.map(group => (
<div key={group.key} className="conversation-group">
<div className="conversation-group-label">
<Text type="secondary">{group.label}</Text>
</div>
{group.conversations.map(conv => (
<div
key={conv.id}
className={`conversation-item ${currentId === conv.id ? 'active' : ''}`}
onClick={() => onSelect(conv.id)}
>
<div className="conversation-item-icon">
{conv.agentIcon ? (
<span className="agent-icon">{conv.agentIcon}</span>
) : (
<MessageOutlined />
)}
</div>
<div className="conversation-item-content">
<div className="conversation-item-title">
{conv.title || '新对话'}
</div>
{conv.lastMessage && (
<div className="conversation-item-preview">
{conv.lastMessage}
</div>
)}
</div>
{onDelete && (
<Dropdown
menu={{ items: getMenuItems(conv) }}
trigger={['click']}
placement="bottomRight"
>
<Button
type="text"
size="small"
icon={<MoreOutlined />}
className="conversation-item-more"
onClick={(e) => e.stopPropagation()}
/>
</Dropdown>
)}
</div>
))}
</div>
))
)}
</div>
</div>
);
};
export default ConversationList;