'use client'; import { useCallback, useRef, useState } from 'react'; import { cn } from '@/lib/cn'; import type { Conversation } from '@/lib/types'; interface ConversationListProps { conversations: Conversation[]; activeId: string | null; onSelect: (id: string) => void; onNew: () => void; onRename: (id: string, title: string) => void; onDelete: (id: string) => void; onArchive: (id: string, archived: boolean) => void; } interface ContextMenuState { conversationId: string; x: number; y: number; } /** Format a date as relative time (e.g. "2h ago", "Yesterday"). */ function formatRelativeTime(dateStr: string): string { const date = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMinutes = Math.floor(diffMs / 60_000); const diffHours = Math.floor(diffMs / 3_600_000); const diffDays = Math.floor(diffMs / 86_400_000); if (diffMinutes < 1) return 'Just now'; if (diffMinutes < 60) return `${diffMinutes}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays === 1) return 'Yesterday'; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } export function ConversationList({ conversations, activeId, onSelect, onNew, onRename, onDelete, onArchive, }: ConversationListProps): React.ReactElement { const [searchQuery, setSearchQuery] = useState(''); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); const [contextMenu, setContextMenu] = useState(null); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [showArchived, setShowArchived] = useState(false); const renameInputRef = useRef(null); const activeConversations = conversations.filter((c) => !c.archived); const archivedConversations = conversations.filter((c) => c.archived); const filteredActive = searchQuery ? activeConversations.filter((c) => (c.title ?? 'Untitled').toLowerCase().includes(searchQuery.toLowerCase()), ) : activeConversations; const filteredArchived = searchQuery ? archivedConversations.filter((c) => (c.title ?? 'Untitled').toLowerCase().includes(searchQuery.toLowerCase()), ) : archivedConversations; const handleContextMenu = useCallback((e: React.MouseEvent, conversationId: string) => { e.preventDefault(); setContextMenu({ conversationId, x: e.clientX, y: e.clientY }); setDeleteConfirmId(null); }, []); const closeContextMenu = useCallback(() => { setContextMenu(null); setDeleteConfirmId(null); }, []); const startRename = useCallback( (id: string, currentTitle: string | null) => { setRenamingId(id); setRenameValue(currentTitle ?? ''); closeContextMenu(); setTimeout(() => renameInputRef.current?.focus(), 0); }, [closeContextMenu], ); const commitRename = useCallback(() => { if (renamingId) { const trimmed = renameValue.trim(); onRename(renamingId, trimmed || 'Untitled'); } setRenamingId(null); setRenameValue(''); }, [renamingId, renameValue, onRename]); const cancelRename = useCallback(() => { setRenamingId(null); setRenameValue(''); }, []); const handleRenameKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter') commitRename(); if (e.key === 'Escape') cancelRename(); }, [commitRename, cancelRename], ); const handleDeleteClick = useCallback((id: string) => { setDeleteConfirmId(id); }, []); const confirmDelete = useCallback( (id: string) => { onDelete(id); setDeleteConfirmId(null); closeContextMenu(); }, [onDelete, closeContextMenu], ); const handleArchiveToggle = useCallback( (id: string, archived: boolean) => { onArchive(id, archived); closeContextMenu(); }, [onArchive, closeContextMenu], ); const contextConv = contextMenu ? conversations.find((c) => c.id === contextMenu.conversationId) : null; function renderConversationItem(conv: Conversation): React.ReactElement { const isActive = activeId === conv.id; const isRenaming = renamingId === conv.id; return (
{isRenaming ? (
setRenameValue(e.target.value)} onBlur={commitRename} onKeyDown={handleRenameKeyDown} className="w-full rounded border border-blue-500 bg-surface-elevated px-2 py-0.5 text-sm text-text-primary outline-none" maxLength={255} />
) : ( )}
); } return ( <> {/* Backdrop to close context menu */} {contextMenu && (