"use client"; import { useState, useMemo, useCallback, useRef, useEffect, Fragment } from "react"; import { useQuery, keepPreviousData } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { ChevronLeft, ChevronRight, ChevronDown, Search, MessageSquare, X, User, Bot, Loader2, } from "lucide-react"; import { formatDate } from "@/lib/utils"; import { LibreChatConversation, LibreChatUser, LibreChatMessage, } from "@/lib/types"; // Hook debounce personnalisé function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; } // Types pour les messages étendus interface ExtendedMessage extends LibreChatMessage { content?: Array<{ type: string; text: string }> | string; parts?: Array; [key: string]: unknown; } // Type pour les groupes d'utilisateurs interface UserGroup { userId: string; conversations: LibreChatConversation[]; totalMessages: number; } // Fetcher générique async function fetchCollection( collection: string, params: Record ): Promise<{ data: T[]; total: number; totalPages: number }> { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== "") { searchParams.set(key, String(value)); } }); const url = `/api/collections/${collection}?${searchParams}`; console.log("[fetchCollection]", collection, "URL:", url); const response = await fetch(url); if (!response.ok) throw new Error(`Erreur lors du chargement de ${collection}`); const data = await response.json(); console.log("[fetchCollection]", collection, "Results:", data.data?.length, "items"); return data; } export function ConversationsTable() { const [page, setPage] = useState(1); const [searchInput, setSearchInput] = useState(""); const [expandedUsers, setExpandedUsers] = useState>(new Set()); const [expandedConversation, setExpandedConversation] = useState(null); const searchInputRef = useRef(null); const usersPerPage = 10; // Nombre de groupes d'utilisateurs par page const debouncedSearch = useDebounce(searchInput, 250); // Reset page et expanded states quand la recherche change useEffect(() => { setPage(1); setExpandedUsers(new Set()); setExpandedConversation(null); }, [debouncedSearch]); // Query conversations avec TanStack Query - limite élevée pour groupement client const { data: conversationsData, isLoading: conversationsLoading, isFetching: conversationsFetching, } = useQuery({ queryKey: ["conversations", debouncedSearch], queryFn: () => fetchCollection("conversations", { page: 1, limit: 1000, // Charger beaucoup pour grouper côté client search: debouncedSearch, }), placeholderData: keepPreviousData, staleTime: 30000, // 30 secondes }); // Query users (cache long car ça change rarement) const { data: usersData } = useQuery({ queryKey: ["users", "all"], queryFn: () => fetchCollection("users", { limit: 1000 }), staleTime: 1000 * 60 * 5, // 5 minutes }); // Query messages de la conversation sélectionnée const { data: messagesData, isLoading: messagesLoading } = useQuery({ queryKey: ["messages", expandedConversation], queryFn: () => fetchCollection("messages", { limit: 500, filter: JSON.stringify({ conversationId: expandedConversation }), }), enabled: !!expandedConversation, staleTime: 30000, }); const conversations = conversationsData?.data ?? []; const total = conversationsData?.total ?? 0; const users = usersData?.data ?? []; const messages = messagesData?.data ?? []; // Map des users pour lookup rapide const userMap = useMemo(() => new Map(users.map((u) => [u._id, u])), [users]); // Grouper les conversations par utilisateur const groupedByUser = useMemo((): UserGroup[] => { const groups: Record = {}; conversations.forEach((conv) => { const uId = String(conv.user); if (!groups[uId]) groups[uId] = []; groups[uId].push(conv); }); return Object.entries(groups) .map(([userId, convs]) => ({ userId, conversations: convs.sort( (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ), totalMessages: convs.reduce((sum, c) => sum + (c.messages?.length || 0), 0), })) .sort((a, b) => { // Trier par date de dernière conversation const aDate = new Date(a.conversations[0]?.updatedAt || 0).getTime(); const bDate = new Date(b.conversations[0]?.updatedAt || 0).getTime(); return bDate - aDate; }); }, [conversations]); // Pagination sur les groupes d'utilisateurs const totalUserGroups = groupedByUser.length; const totalPages = Math.ceil(totalUserGroups / usersPerPage); const paginatedGroups = groupedByUser.slice( (page - 1) * usersPerPage, page * usersPerPage ); // Toggle user group expansion const toggleUserExpanded = useCallback((userId: string) => { setExpandedUsers((prev) => { const next = new Set(prev); if (next.has(userId)) { next.delete(userId); // Fermer aussi la conversation si elle appartient à cet utilisateur setExpandedConversation(null); } else { next.add(userId); } return next; }); }, []); // Toggle conversation expansion const toggleConversationExpanded = useCallback((conversationId: string) => { console.log("[Conversations] Toggle conversation:", conversationId); setExpandedConversation((prev) => prev === conversationId ? null : conversationId ); }, []); const getUserInfo = useCallback( (userId: string) => { const user = userMap.get(userId); return { name: user?.name || user?.username || `User ${userId.slice(-6)}`, email: user?.email || null, }; }, [userMap] ); // Extraction du contenu des messages const getMessageContent = useCallback((message: LibreChatMessage): string => { const msg = message as ExtendedMessage; if (msg.content && Array.isArray(msg.content)) { for (const item of msg.content) { if (item?.text) return item.text.trim(); } } if (message.text?.trim()) return message.text.trim(); if (typeof msg.content === "string" && msg.content.trim()) return msg.content.trim(); if (msg.parts?.[0]) { const part = msg.parts[0]; if (typeof part === "string") return part.trim(); if (part?.text) return part.text.trim(); } return "Contenu non disponible"; }, []); const clearSearch = () => { setSearchInput(""); searchInputRef.current?.focus(); }; const isSearching = searchInput !== debouncedSearch || conversationsFetching; return (
Conversations {total} {conversationsFetching && !conversationsLoading && ( )} {/* Barre de recherche */}
setSearchInput(e.target.value)} className="pl-10 pr-10" />
{isSearching ? ( ) : searchInput ? ( ) : null}
{debouncedSearch && (

{total} résultat{total > 1 ? "s" : ""} pour "{debouncedSearch}"

)}
{conversationsLoading ? (
{Array.from({ length: 5 }).map((_, i) => (
))}
) : conversations.length === 0 ? (
{debouncedSearch ? ( <>

Aucun résultat pour "{debouncedSearch}"

) : (

Aucune conversation

)}
) : ( <> {/* Liste des groupes d'utilisateurs */}
{paginatedGroups.map((group) => { const userInfo = getUserInfo(group.userId); const isUserExpanded = expandedUsers.has(group.userId); return (
{/* Header du groupe utilisateur */} {/* Liste des conversations de l'utilisateur */} {isUserExpanded && (
{group.conversations.map((conv) => { const msgCount = conv.messages?.length || 0; const isConvExpanded = expandedConversation === conv.conversationId; return ( {/* Messages de la conversation */} {isConvExpanded && (
{messagesLoading ? (
Chargement des messages...
) : messages.length === 0 ? (

Aucun message dans cette conversation

) : (
{messages .sort( (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ) .map((msg) => { const content = getMessageContent(msg); const isUser = msg.isCreatedByUser; return (
{isUser ? ( ) : ( )}
{isUser ? "Utilisateur" : "Assistant"} {formatDate(msg.createdAt)} {msg.tokenCount > 0 && ( ({msg.tokenCount} tokens) )}
{content}
); })}
)}
)}
); })}
)}
); })}
{/* Pagination */} {totalPages > 1 && (

Page {page} / {totalPages} ({totalUserGroups} utilisateurs)

)} )}
); }