Files
deerflow-code/web-code/components/angentchat/SixStepChatHistory.tsx
T
2026-05-27 19:43:15 +08:00

347 lines
13 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import { Check, Eraser, MessageSquareText, Pencil, Plus, Trash2, X } from 'lucide-react';
export interface SixStepConversationMeta {
id: string;
title: string;
updatedAt: number;
}
interface Props {
isOpen: boolean;
isDark: boolean;
conversations: SixStepConversationMeta[];
activeId: string | null;
onClose: () => void;
onOpen: () => void;
onSelect: (id: string) => void;
onNew: () => void;
onDelete: (id: string) => void;
onRename: (id: string, title: string) => void;
onClearAll: () => void;
}
const UNTITLED = '未命名对话';
const SixStepChatHistory: React.FC<Props> = ({
isOpen,
isDark,
conversations,
activeId,
onClose,
onOpen,
onSelect,
onNew,
onDelete,
onRename,
onClearAll,
}) => {
const [editingId, setEditingId] = useState<string | null>(null);
const [draftTitle, setDraftTitle] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (editingId && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editingId]);
const startEdit = (conversation: SixStepConversationMeta) => {
setEditingId(conversation.id);
setDraftTitle(conversation.title);
};
const commitEdit = () => {
if (!editingId) return;
onRename(editingId, draftTitle.trim() || UNTITLED);
setEditingId(null);
setDraftTitle('');
};
const cancelEdit = () => {
setEditingId(null);
setDraftTitle('');
};
const handleDelete = (conversation: SixStepConversationMeta) => {
const ok = window.confirm(`确认删除对话“${conversation.title || UNTITLED}”吗?`);
if (!ok) return;
onDelete(conversation.id);
};
const handleClearAll = () => {
if (conversations.length === 0) return;
const ok = window.confirm('确认清空当前对话列表中的全部历史记录吗?此操作无法撤销。');
if (!ok) return;
onClearAll();
};
const railButtonClass = (active = false) =>
`flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl border transition ${
active
? isDark
? 'border-cyan-400/40 bg-cyan-500/10 text-cyan-200 shadow-sm'
: 'border-red-200 bg-red-50/70 text-red-500 shadow-sm'
: isDark
? 'border-slate-700 bg-slate-800 text-slate-400 hover:bg-slate-700 hover:text-slate-100'
: 'border-gray-200 bg-white text-gray-500 hover:bg-gray-50 hover:text-gray-900'
}`;
const handleCollapsedSelect = (id: string) => {
onOpen();
window.setTimeout(() => {
onSelect(id);
}, 120);
};
const handleCollapsedNew = () => {
onOpen();
window.setTimeout(() => {
onNew();
}, 120);
};
return (
<aside
className={`flex h-full shrink-0 flex-col overflow-hidden border-r transition-[width,opacity] duration-300 ease-out ${
isOpen ? 'pointer-events-auto w-[300px] opacity-100' : 'pointer-events-auto w-[72px] opacity-100'
} ${
isDark
? 'border-slate-700/50 bg-slate-900/60 text-slate-100'
: 'border-red-100 bg-gray-50/70 text-gray-900'
}`}
>
{isOpen ? (
<>
<div
className={`border-b px-4 py-4 transition-all duration-300 ease-out ${isDark ? 'border-slate-700/50' : 'border-red-100'} ${
isOpen ? 'translate-x-0 opacity-100' : '-translate-x-2 opacity-0'
}`}
>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<h3 className="text-sm font-semibold">聊天记录</h3>
<p className={`mt-1 text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
查看和管理当前页面的历史对话
</p>
</div>
<button
type="button"
onClick={onClose}
title="收起聊天记录"
className={`flex h-8 w-8 items-center justify-center rounded-lg transition ${
isDark
? 'text-slate-400 hover:bg-slate-800 hover:text-slate-100'
: 'text-gray-400 hover:bg-gray-100 hover:text-gray-700'
}`}
>
<X className="h-4 w-4" />
</button>
</div>
<div className="mt-4 grid grid-cols-2 gap-2">
<button
type="button"
onClick={onNew}
className={`inline-flex h-9 items-center justify-center gap-2 rounded-lg border text-sm font-medium transition ${
isDark
? 'border-slate-700 bg-slate-800 text-slate-100 hover:bg-slate-700'
: 'border-red-200 bg-white text-red-600 hover:bg-red-50'
}`}
>
<Plus className="h-4 w-4" />
新建对话
</button>
<button
type="button"
onClick={handleClearAll}
disabled={conversations.length === 0}
className={`inline-flex h-9 items-center justify-center gap-2 rounded-lg border text-sm font-medium transition disabled:cursor-not-allowed disabled:opacity-50 ${
isDark
? 'border-slate-700 bg-slate-800 text-slate-100 hover:bg-slate-700'
: 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50'
}`}
>
<Eraser className="h-4 w-4" />
清空记录
</button>
</div>
</div>
<div
className={`min-h-0 flex-1 space-y-1 overflow-y-auto px-3 py-3 transition-all duration-300 ease-out ${
isOpen ? 'translate-x-0 opacity-100' : '-translate-x-2 opacity-0'
}`}
>
{conversations.length === 0 ? (
<div
className={`rounded-xl border border-dashed px-3 py-8 text-center text-xs ${
isDark ? 'border-slate-700 text-slate-500' : 'border-gray-200 text-gray-400'
}`}
>
还没有任何对话记录
</div>
) : (
conversations.map((conversation) => {
const isActive = activeId === conversation.id;
const isEditing = editingId === conversation.id;
return (
<div
key={conversation.id}
role={isEditing ? undefined : 'button'}
tabIndex={isEditing ? -1 : 0}
onClick={() => !isEditing && onSelect(conversation.id)}
onKeyDown={(event) => {
if (!isEditing && (event.key === 'Enter' || event.key === ' ')) {
event.preventDefault();
onSelect(conversation.id);
}
}}
className={`group flex cursor-pointer items-center gap-2 rounded-xl border px-3 py-2.5 text-left transition-all duration-200 ${
isActive
? isDark
? 'border-cyan-400/40 bg-cyan-500/10 text-slate-50 shadow-sm'
: 'border-red-200 bg-red-50/70 text-gray-900 shadow-sm'
: isDark
? 'border-transparent text-slate-300 hover:border-slate-700 hover:bg-slate-800/60 hover:text-slate-100'
: 'border-transparent text-gray-700 hover:border-gray-200 hover:bg-white hover:text-gray-900'
}`}
>
<span
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${
isActive
? isDark
? 'bg-cyan-500/20 text-cyan-200'
: 'bg-white text-red-500 ring-1 ring-red-100'
: isDark
? 'bg-slate-800 text-slate-400'
: 'bg-gray-100 text-gray-500'
}`}
>
<MessageSquareText className="h-4 w-4" />
</span>
<div className="min-w-0 flex-1">
{isEditing ? (
<input
ref={inputRef}
value={draftTitle}
onChange={(event) => setDraftTitle(event.target.value)}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
commitEdit();
} else if (event.key === 'Escape') {
event.preventDefault();
cancelEdit();
}
}}
onBlur={commitEdit}
className={`w-full border-b bg-transparent text-sm font-medium outline-none ${
isDark ? 'border-cyan-400/60 text-white' : 'border-red-300 text-gray-900'
}`}
/>
) : (
<div className="truncate text-sm font-medium">{conversation.title || UNTITLED}</div>
)}
</div>
{isEditing ? (
<button
type="button"
onClick={(event) => {
event.stopPropagation();
commitEdit();
}}
title="保存"
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition ${
isDark
? 'text-emerald-300 hover:bg-emerald-500/15'
: 'text-emerald-600 hover:bg-emerald-50'
}`}
>
<Check className="h-4 w-4" />
</button>
) : (
<>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
startEdit(conversation);
}}
title="重命名"
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition ${
isActive ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
} ${
isDark
? 'text-slate-400 hover:bg-slate-800 hover:text-slate-100'
: 'text-gray-400 hover:bg-gray-100 hover:text-gray-700'
}`}
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
handleDelete(conversation);
}}
title="删除对话"
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition ${
isActive ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
} ${
isDark
? 'text-slate-400 hover:bg-rose-500/15 hover:text-rose-300'
: 'text-gray-400 hover:bg-rose-50 hover:text-rose-500'
}`}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</>
)}
</div>
);
})
)}
</div>
</>
) : (
<div className="flex min-h-0 flex-1 flex-col items-center gap-3 px-2 py-4">
<button
type="button"
onClick={handleCollapsedNew}
title="新建对话"
className={`${railButtonClass(false)} sixstep-fade-up`}
>
<Plus className="h-5 w-5" />
</button>
<div className="min-h-0 w-full flex-1 overflow-y-auto">
<div className="flex flex-col items-center gap-2">
{conversations.map((conversation, index) => (
<button
key={conversation.id}
type="button"
onClick={() => handleCollapsedSelect(conversation.id)}
title={conversation.title || UNTITLED}
className={`${railButtonClass(activeId === conversation.id)} transition-all duration-300 ease-out`}
style={{ transitionDelay: `${Math.min(index, 6) * 25}ms` }}
>
<MessageSquareText className="h-4 w-4" />
</button>
))}
</div>
</div>
</div>
)}
</aside>
);
};
export default SixStepChatHistory;