代码修改
This commit is contained in:
1 parent
8f5af46ec2
commit
8f75e12871
168 files changed
+76575
No files matched your search
@@ -0,0 +1,346 @@
|
||||
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;
|
||||
Reference in new issue
Block a user