'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; isOpen: boolean; onClose: () => void; 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; } 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, isOpen, onClose, 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((conversation) => !conversation.archived); const archivedConversations = conversations.filter((conversation) => conversation.archived); const filteredActive = searchQuery ? activeConversations.filter((conversation) => (conversation.title ?? 'Untitled').toLowerCase().includes(searchQuery.toLowerCase()), ) : activeConversations; const filteredArchived = searchQuery ? archivedConversations.filter((conversation) => (conversation.title ?? 'Untitled').toLowerCase().includes(searchQuery.toLowerCase()), ) : archivedConversations; const handleContextMenu = useCallback((event: React.MouseEvent, conversationId: string) => { event.preventDefault(); setContextMenu({ conversationId, x: event.clientX, y: event.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(''); }, [onRename, renameValue, renamingId]); const cancelRename = useCallback(() => { setRenamingId(null); setRenameValue(''); }, []); const handleRenameKeyDown = useCallback( (event: React.KeyboardEvent) => { if (event.key === 'Enter') commitRename(); if (event.key === 'Escape') cancelRename(); }, [cancelRename, commitRename], ); const confirmDelete = useCallback( (id: string) => { onDelete(id); setDeleteConfirmId(null); closeContextMenu(); }, [closeContextMenu, onDelete], ); const handleArchiveToggle = useCallback( (id: string, archived: boolean) => { onArchive(id, archived); closeContextMenu(); }, [closeContextMenu, onArchive], ); const contextConversation = contextMenu ? conversations.find((conversation) => conversation.id === contextMenu.conversationId) : null; function renderConversationItem(conversation: Conversation): React.ReactElement { const isActive = activeId === conversation.id; const isRenaming = renamingId === conversation.id; return (
{isRenaming ? (
setRenameValue(event.target.value)} onBlur={commitRename} onKeyDown={handleRenameKeyDown} className="w-full rounded-xl border px-3 py-2 text-sm outline-none" style={{ borderColor: 'var(--color-ms-blue-500)', backgroundColor: 'var(--color-surface-2)', color: 'var(--color-text)', }} maxLength={255} />
) : ( )}
); } return ( <> {isOpen ? (
setSearchQuery(event.target.value)} placeholder="Search conversations…" className="w-full rounded-2xl border px-3 py-2 text-xs placeholder:text-[var(--color-muted)] focus:outline-none" style={{ backgroundColor: 'var(--color-surface-2)', borderColor: 'var(--color-border)', color: 'var(--color-text)', }} />
{filteredActive.length === 0 && !searchQuery ? (

No conversations yet

) : null} {filteredActive.length === 0 && searchQuery ? (

No results for “{searchQuery}”

) : null} {filteredActive.map((conversation) => renderConversationItem(conversation))} {archivedConversations.length > 0 ? (
{showArchived ? (
{filteredArchived.map((conversation) => renderConversationItem(conversation))}
) : null}
) : null}
{contextMenu && contextConversation ? (
{deleteConfirmId === contextConversation.id ? ( ) : ( )}
) : null} ); }