// ========================================== // Chat Surface — SidebarChats Component // ========================================== // Personal AI chats with folder grouping and context menus. const html = window.html; const { useState, useCallback, useRef, useEffect } = window.hooks; const CHEVRON_SVG = html` `; const CHAT_SVG = html` `; const FOLDER_SVG = html` `; const DOTS_SVG = html` `; /** * @param {{ * chatsByFolder: Record, * folders: Array<{id: string, name: string}>, * activeId: string|null, * collapsed: boolean, * onToggle: () => void, * onSelect: (id: string) => void, * onRename: (id: string, title: string) => void, * onDelete: (id: string) => void, * onMoveToFolder: (chatId: string, folderId: string|null) => void, * onCreateFolder: (name: string) => void, * onRenameFolder: (id: string, name: string) => void, * onDeleteFolder: (id: string) => void, * }} props */ export function SidebarChats({ chatsByFolder, folders, activeId, collapsed, onToggle, onSelect, onRename, onDelete, onMoveToFolder, onCreateFolder, onRenameFolder, onDeleteFolder, }) { const [contextMenu, setContextMenu] = useState(null); const menuRef = useRef(null); // Close context menu on outside click or Escape useEffect(() => { if (!contextMenu) return; function _close(e) { if (e.type === 'keydown' && e.key !== 'Escape') return; if (e.type === 'click' && menuRef.current?.contains(e.target)) return; setContextMenu(null); } document.addEventListener('click', _close); document.addEventListener('keydown', _close); return () => { document.removeEventListener('click', _close); document.removeEventListener('keydown', _close); }; }, [contextMenu]); const _openMenu = useCallback((e, type, item) => { e.preventDefault(); e.stopPropagation(); const rect = e.currentTarget.getBoundingClientRect(); setContextMenu({ type, item, x: rect.right, y: rect.bottom }); }, []); const _action = useCallback(async (action, item) => { setContextMenu(null); if (action === 'rename') { const newTitle = prompt('Rename:', item.title || item.name || ''); if (newTitle && newTitle.trim()) { if (item.name !== undefined) await onRenameFolder(item.id, newTitle.trim()); else await onRename(item.id, newTitle.trim()); } } else if (action === 'delete') { const label = item.name !== undefined ? 'folder' : 'chat'; if (confirm('Delete this ' + label + '?')) { if (item.name !== undefined) await onDeleteFolder(item.id); else await onDelete(item.id); } } else if (action === 'move-out') { await onMoveToFolder(item.id, null); } else if (action === 'new-folder') { const name = prompt('Folder name:'); if (name && name.trim()) await onCreateFolder(name.trim()); } else if (action.startsWith('move-to:')) { const fid = action.slice(8); await onMoveToFolder(item.id, fid); } }, [onRename, onDelete, onMoveToFolder, onCreateFolder, onRenameFolder, onDeleteFolder]); // Count total chats const totalChats = Object.values(chatsByFolder).reduce((sum, arr) => sum + arr.length, 0); const unfolderedChats = chatsByFolder[''] || []; return html`
${!collapsed && html`
${/* Folders first */''} ${folders.map(folder => { const folderChats = chatsByFolder[folder.id] || []; return html`
${FOLDER_SVG} ${folder.name} ${folderChats.length}
${folderChats.map(ch => html` <${ChatItem} key=${ch.id} chat=${ch} activeId=${activeId} onSelect=${onSelect} onOpenMenu=${_openMenu} indent=${true} />`)}
`; })} ${/* Unfoldered chats */''} ${unfolderedChats.map(ch => html` <${ChatItem} key=${ch.id} chat=${ch} activeId=${activeId} onSelect=${onSelect} onOpenMenu=${_openMenu} />`)} ${totalChats === 0 && html`
No chats yet
`}
`} ${/* Context Menu */''} ${contextMenu && html`
${contextMenu.type === 'chat' && html` ${folders.length > 0 && folders.map(f => html` `)} ${contextMenu.item.folder_id && html` `} `} ${contextMenu.type === 'folder' && html` `}
`}
`; } function ChatItem({ chat, activeId, onSelect, onOpenMenu, indent }) { return html` `; } function _stopAndOpen(e, onOpenMenu, type, item) { e.stopPropagation(); onOpenMenu(e, type, item); }