// ============================================================ // SessionSidebar — 会话历史侧边栏(Overlay 抽屉式) // 职责:展示会话列表、搜索、新建/切换/删除/重命名会话 // 设计:overlay 覆盖在主界面之上,不挤压主界面空间 // ============================================================ import { useMemo, useState } from "react"; import { useI18n } from "../../lib/i18n"; import type { SessionSummary } from "../../types"; interface SessionSidebarProps { sessions: SessionSummary[]; activeSessionId: string | null; open: boolean; onClose: () => void; onNewSession: () => void; onSelectSession: (id: string) => void; onDeleteSession: (id: string) => void; onRenameSession: (id: string, title: string) => void; } /** 格式化相对时间 */ function formatRelativeTime(ts: number): string { const now = Date.now(); const diff = now - ts; const min = 60 * 1000; const hour = 60 * min; const day = 24 * hour; if (diff < min) return "刚刚"; if (diff < hour) return `${Math.floor(diff / min)}分钟前`; if (diff < day) return `${Math.floor(diff / hour)}小时前`; if (diff < 2 * day) return "昨天"; if (diff < 7 * day) return `${Math.floor(diff / day)}天前`; const d = new Date(ts); return `${d.getMonth() + 1}/${d.getDate()}`; } /** 判断两个时间戳是否为同一天 */ function isSameDay(ts: number, ref: Date): boolean { const d = new Date(ts); return d.getFullYear() === ref.getFullYear() && d.getMonth() === ref.getMonth() && d.getDate() === ref.getDate(); } type TimeGroup = "today" | "yesterday" | "earlier"; function getTimeGroup(ts: number, today: Date, yesterday: Date): TimeGroup { if (isSameDay(ts, today)) return "today"; if (isSameDay(ts, yesterday)) return "yesterday"; return "earlier"; } export function SessionSidebar({ sessions, activeSessionId, open, onClose, onNewSession, onSelectSession, onDeleteSession, onRenameSession, }: SessionSidebarProps) { const { t } = useI18n(); const [editingId, setEditingId] = useState(null); const [editTitle, setEditTitle] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const handleStartRename = (id: string, currentTitle: string) => { setEditingId(id); setEditTitle(currentTitle); }; const handleConfirmRename = () => { if (editingId && editTitle.trim()) { onRenameSession(editingId, editTitle.trim()); } setEditingId(null); setEditTitle(""); }; const handleCancelRename = () => { setEditingId(null); setEditTitle(""); }; const handleSelect = (id: string) => { onSelectSession(id); // 选择后自动收起侧边栏 onClose(); }; // 过滤 + 分组 const grouped = useMemo(() => { const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const filtered = searchQuery.trim() ? sessions.filter((s) => s.title.toLowerCase().includes(searchQuery.toLowerCase()) || s.preview.toLowerCase().includes(searchQuery.toLowerCase()) ) : sessions; const groups: Record = { today: [], yesterday: [], earlier: [], }; for (const session of filtered) { const group = getTimeGroup(session.lastActiveAt, today, yesterday); groups[group].push(session); } return groups; }, [sessions, searchQuery]); const groupLabels: Record = { today: t("sidebar.today"), yesterday: t("sidebar.yesterday"), earlier: t("sidebar.earlier"), }; return ( <>
{/* 头部:新建 + 收起 */}
{/* 搜索栏 */}
setSearchQuery(e.target.value)} />
{/* 会话列表(按时间分组) */}
{sessions.length === 0 ? (
{t("sidebar.empty")}
) : ( (["today", "yesterday", "earlier"] as TimeGroup[]).map((groupKey) => { const items = grouped[groupKey]; if (items.length === 0) return null; return (
{groupLabels[groupKey]}
{items.map((session) => (
handleSelect(session.id)} > {editingId === session.id ? (
e.stopPropagation()}> setEditTitle(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleConfirmRename(); if (e.key === "Escape") handleCancelRename(); }} autoFocus />
) : ( <> {/* 视频标记图标 */} {session.messageCount > 5 && ( )}
{session.title}
{session.messageCount > 0 && ( {session.messageCount}{t("sidebar.messages")} )} {formatRelativeTime(session.lastActiveAt)}
)}
))}
); }) )}
); }