"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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; 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; } // 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 response = await fetch(`/api/collections/${collection}?${searchParams}`); if (!response.ok) throw new Error(`Erreur lors du chargement de ${collection}`); return response.json(); } export function ConversationsTable() { const [page, setPage] = useState(1); const [searchInput, setSearchInput] = useState(""); const [selectedConversationId, setSelectedConversationId] = useState(null); const searchInputRef = useRef(null); const limit = 20; const debouncedSearch = useDebounce(searchInput, 250); // Reset page quand la recherche change useEffect(() => { setPage(1); }, [debouncedSearch]); // Query conversations avec TanStack Query const { data: conversationsData, isLoading: conversationsLoading, isFetching: conversationsFetching, } = useQuery({ queryKey: ["conversations", page, limit, debouncedSearch], queryFn: () => fetchCollection("conversations", { page, limit, 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", selectedConversationId], queryFn: () => fetchCollection("messages", { limit: 500, filter: JSON.stringify({ conversationId: selectedConversationId }), }), enabled: !!selectedConversationId, staleTime: 30000, }); const conversations = conversationsData?.data ?? []; const total = conversationsData?.total ?? 0; const totalPages = conversationsData?.totalPages ?? 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]); 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

)}
) : ( <>
Utilisateur Titre Endpoint Msgs Statut Date {conversations.map((conv) => { const userInfo = getUserInfo(String(conv.user)); const msgCount = conv.messages?.length || 0; const isSelected = selectedConversationId === conv.conversationId; return ( setSelectedConversationId( isSelected ? null : conv.conversationId ) } >
{userInfo.name} {userInfo.email && ( {userInfo.email} )}
{String(conv.title) || "Sans titre"} {String(conv.endpoint).slice(0, 10)} {msgCount} {conv.isArchived ? "Archivée" : "Active"} {formatDate(conv.updatedAt)}
{/* Messages inline sous la row */} {isSelected && (
{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}

)} )}
); }