import { useEffect, useRef, useState } from "react"; import { Plus, Search, X } from "lucide-react"; import { useChatConversations } from "../hooks/useChatConversations"; import { useChatPanel } from "../context/ChatPanelContext"; import { ChatConversationItem } from "./ChatConversationItem"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import type { ChatConversationListItem } from "@paperclipai/shared"; interface ChatConversationListProps { companyId: string; } export function ChatConversationList({ companyId }: ChatConversationListProps) { const { activeConversationId, setActiveConversationId } = useChatPanel(); const [searchTerm, setSearchTerm] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const searchInputRef = useRef(null); useEffect(() => { const timer = setTimeout(() => setDebouncedSearch(searchTerm), 300); return () => clearTimeout(timer); }, [searchTerm]); // Listen for focus-chat-search custom event (dispatched by Cmd+K in Layout) useEffect(() => { const handler = () => searchInputRef.current?.focus(); window.addEventListener("nexus:focus-chat-search", handler); return () => window.removeEventListener("nexus:focus-chat-search", handler); }, []); const { data, isLoading, hasNextPage, fetchNextPage, createMutation, updateMutation, deleteMutation } = useChatConversations(companyId, { search: debouncedSearch || undefined }); const [deletingId, setDeletingId] = useState(null); const sentinelRef = useRef(null); // Infinite scroll via IntersectionObserver useEffect(() => { const sentinel = sentinelRef.current; if (!sentinel) return; const observer = new IntersectionObserver( (entries) => { if (entries[0]?.isIntersecting && hasNextPage) { void fetchNextPage(); } }, { threshold: 0.1 }, ); observer.observe(sentinel); return () => observer.disconnect(); }, [hasNextPage, fetchNextPage]); const allConversations: ChatConversationListItem[] = data?.pages.flatMap((p) => p.items) ?? []; // Separate pinned from unpinned const pinned = allConversations .filter((c) => c.pinnedAt) .sort((a, b) => (b.pinnedAt! > a.pinnedAt! ? 1 : -1)); const unpinned = allConversations .filter((c) => !c.pinnedAt) .sort((a, b) => (b.updatedAt > a.updatedAt ? 1 : -1)); const sorted = [...pinned, ...unpinned]; const handleNewConversation = async () => { try { const newConvo = await createMutation.mutateAsync(undefined); setActiveConversationId(newConvo.id); } catch { // ignore -- error will surface via mutation state if needed } }; const handleRename = (id: string, title: string) => { updateMutation.mutate({ id, title }); }; const handlePin = (id: string, pinned: boolean) => { updateMutation.mutate({ id, pinnedAt: pinned ? new Date().toISOString() : null }); }; const handleArchive = (id: string) => { updateMutation.mutate({ id, archivedAt: new Date().toISOString() }); }; const handleDeleteRequest = (id: string) => { setDeletingId(id); }; const handleDeleteConfirm = () => { if (!deletingId) return; deleteMutation.mutate(deletingId, { onSuccess: () => { if (activeConversationId === deletingId) { setActiveConversationId(null); } setDeletingId(null); }, }); }; return (
{/* New conversation button */}
{/* Search input */}
setSearchTerm(e.target.value)} placeholder="Search conversations..." className="h-7 pl-7 pr-7 text-xs" /> {searchTerm && ( )}
{isLoading ? ( // Loading skeletons Array.from({ length: 5 }).map((_, i) => ( )) ) : sorted.length === 0 ? (

No conversations yet

Start a conversation to get help from your agents.

) : ( sorted.map((conversation) => ( )) )} {/* Infinite scroll sentinel */}
{/* Delete confirmation dialog */} !open && setDeletingId(null)}> Delete conversation? This conversation and all its messages will be permanently deleted.
); }