conversation clan
This commit is contained in:
@@ -10,7 +10,9 @@
|
||||
"Bash(git push)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(node debug-search.js:*)",
|
||||
"Bash(node update-referent.js:*)"
|
||||
"Bash(node update-referent.js:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(curl:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -51,6 +51,12 @@ export async function GET(
|
||||
const limit = parseInt(searchParams.get("limit") || "20");
|
||||
const filter = JSON.parse(searchParams.get("filter") || "{}");
|
||||
|
||||
// Debug logging pour messages
|
||||
if (collection === "messages") {
|
||||
console.log("[API Messages] Filter reçu:", JSON.stringify(filter));
|
||||
console.log("[API Messages] Raw filter param:", searchParams.get("filter"));
|
||||
}
|
||||
|
||||
// Gestion spéciale pour la collection users avec recherche par email ou id
|
||||
if (collection === "users") {
|
||||
const email = searchParams.get("email");
|
||||
@@ -141,6 +147,11 @@ export async function GET(
|
||||
db.collection(collection).countDocuments(filter),
|
||||
]);
|
||||
|
||||
// Debug logging pour messages
|
||||
if (collection === "messages") {
|
||||
console.log("[API Messages] Résultats:", data.length, "messages, total:", total);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data,
|
||||
total,
|
||||
|
||||
@@ -3,14 +3,6 @@
|
||||
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";
|
||||
@@ -51,6 +43,13 @@ interface ExtendedMessage extends LibreChatMessage {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Type pour les groupes d'utilisateurs
|
||||
interface UserGroup {
|
||||
userId: string;
|
||||
conversations: LibreChatConversation[];
|
||||
totalMessages: number;
|
||||
}
|
||||
|
||||
// Fetcher générique
|
||||
async function fetchCollection<T>(
|
||||
collection: string,
|
||||
@@ -63,36 +62,44 @@ async function fetchCollection<T>(
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/collections/${collection}?${searchParams}`);
|
||||
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}`);
|
||||
return response.json();
|
||||
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 [selectedConversationId, setSelectedConversationId] = useState<string | null>(null);
|
||||
const [expandedUsers, setExpandedUsers] = useState<Set<string>>(new Set());
|
||||
const [expandedConversation, setExpandedConversation] = useState<string | null>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const limit = 20;
|
||||
const usersPerPage = 10; // Nombre de groupes d'utilisateurs par page
|
||||
const debouncedSearch = useDebounce(searchInput, 250);
|
||||
|
||||
// Reset page quand la recherche change
|
||||
// Reset page et expanded states quand la recherche change
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
setExpandedUsers(new Set());
|
||||
setExpandedConversation(null);
|
||||
}, [debouncedSearch]);
|
||||
|
||||
// Query conversations avec TanStack Query
|
||||
// Query conversations avec TanStack Query - limite élevée pour groupement client
|
||||
const {
|
||||
data: conversationsData,
|
||||
isLoading: conversationsLoading,
|
||||
isFetching: conversationsFetching,
|
||||
} = useQuery({
|
||||
queryKey: ["conversations", page, limit, debouncedSearch],
|
||||
queryKey: ["conversations", debouncedSearch],
|
||||
queryFn: () =>
|
||||
fetchCollection<LibreChatConversation>("conversations", {
|
||||
page,
|
||||
limit,
|
||||
page: 1,
|
||||
limit: 1000, // Charger beaucoup pour grouper côté client
|
||||
search: debouncedSearch,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
@@ -108,25 +115,79 @@ export function ConversationsTable() {
|
||||
|
||||
// Query messages de la conversation sélectionnée
|
||||
const { data: messagesData, isLoading: messagesLoading } = useQuery({
|
||||
queryKey: ["messages", selectedConversationId],
|
||||
queryKey: ["messages", expandedConversation],
|
||||
queryFn: () =>
|
||||
fetchCollection<LibreChatMessage>("messages", {
|
||||
limit: 500,
|
||||
filter: JSON.stringify({ conversationId: selectedConversationId }),
|
||||
filter: JSON.stringify({ conversationId: expandedConversation }),
|
||||
}),
|
||||
enabled: !!selectedConversationId,
|
||||
enabled: !!expandedConversation,
|
||||
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]);
|
||||
|
||||
// Grouper les conversations par utilisateur
|
||||
const groupedByUser = useMemo((): UserGroup[] => {
|
||||
const groups: Record<string, LibreChatConversation[]> = {};
|
||||
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);
|
||||
@@ -236,88 +297,91 @@ export function ConversationsTable() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-52">Utilisateur</TableHead>
|
||||
<TableHead>Titre</TableHead>
|
||||
<TableHead className="w-24">Endpoint</TableHead>
|
||||
<TableHead className="w-20 text-center">Msgs</TableHead>
|
||||
<TableHead className="w-24">Statut</TableHead>
|
||||
<TableHead className="w-28">Date</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{conversations.map((conv) => {
|
||||
const userInfo = getUserInfo(String(conv.user));
|
||||
const msgCount = conv.messages?.length || 0;
|
||||
const isSelected = selectedConversationId === conv.conversationId;
|
||||
{/* Liste des groupes d'utilisateurs */}
|
||||
<div className="space-y-2">
|
||||
{paginatedGroups.map((group) => {
|
||||
const userInfo = getUserInfo(group.userId);
|
||||
const isUserExpanded = expandedUsers.has(group.userId);
|
||||
|
||||
return (
|
||||
<Fragment key={conv._id}>
|
||||
<TableRow
|
||||
className={`cursor-pointer hover:bg-muted/50 transition-colors ${isSelected ? "bg-muted/50" : ""}`}
|
||||
onClick={() =>
|
||||
setSelectedConversationId(
|
||||
isSelected ? null : conv.conversationId
|
||||
)
|
||||
}
|
||||
<div key={group.userId} className="border rounded-lg overflow-hidden">
|
||||
{/* Header du groupe utilisateur */}
|
||||
<button
|
||||
onClick={() => toggleUserExpanded(group.userId)}
|
||||
className="w-full flex items-center gap-3 p-4 bg-muted/30 hover:bg-muted/50 transition-colors text-left"
|
||||
>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-muted-foreground transition-transform ${
|
||||
isSelected ? "rotate-0" : "-rotate-90"
|
||||
className={`h-5 w-5 text-muted-foreground transition-transform flex-shrink-0 ${
|
||||
isUserExpanded ? "rotate-0" : "-rotate-90"
|
||||
}`}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium truncate max-w-[180px]">
|
||||
{userInfo.name}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-semibold">{userInfo.name}</span>
|
||||
{userInfo.email && (
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[180px]">
|
||||
{userInfo.email}
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({userInfo.email})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="truncate block max-w-xs" title={String(conv.title)}>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Badge variant="secondary">
|
||||
{group.conversations.length} conversation{group.conversations.length > 1 ? "s" : ""}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{group.totalMessages} msg{group.totalMessages > 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Liste des conversations de l'utilisateur */}
|
||||
{isUserExpanded && (
|
||||
<div className="border-t">
|
||||
{group.conversations.map((conv) => {
|
||||
const msgCount = conv.messages?.length || 0;
|
||||
const isConvExpanded = expandedConversation === conv.conversationId;
|
||||
|
||||
return (
|
||||
<Fragment key={conv._id}>
|
||||
<button
|
||||
onClick={() => toggleConversationExpanded(conv.conversationId)}
|
||||
className={`w-full flex items-center gap-3 p-3 pl-10 hover:bg-muted/30 transition-colors text-left border-b last:border-b-0 ${
|
||||
isConvExpanded ? "bg-muted/20" : ""
|
||||
}`}
|
||||
>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-muted-foreground transition-transform flex-shrink-0 ${
|
||||
isConvExpanded ? "rotate-0" : "-rotate-90"
|
||||
}`}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="truncate block text-sm font-medium">
|
||||
{String(conv.title) || "Sans titre"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Badge variant="outline" className="text-xs font-mono">
|
||||
{String(conv.endpoint).slice(0, 10)}
|
||||
{String(conv.endpoint).slice(0, 8)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{msgCount}
|
||||
{msgCount} msg{msgCount > 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={conv.isArchived ? "outline" : "default"}
|
||||
className="text-xs"
|
||||
>
|
||||
{conv.isArchived ? "Archivée" : "Active"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDate(conv.updatedAt)}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Messages inline sous la row */}
|
||||
{isSelected && (
|
||||
<TableRow key={`${conv._id}-messages`}>
|
||||
<TableCell colSpan={6} className="p-0 border-b">
|
||||
<div className="bg-slate-50 p-4">
|
||||
{/* Messages de la conversation */}
|
||||
{isConvExpanded && (
|
||||
<div className="bg-slate-50 p-4 pl-14 border-b">
|
||||
{messagesLoading ? (
|
||||
<div className="flex items-center justify-center py-8 gap-2 text-muted-foreground">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
@@ -379,21 +443,22 @@ export function ConversationsTable() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Page {page} / {totalPages}
|
||||
Page {page} / {totalPages} ({totalUserGroups} utilisateurs)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -421,7 +486,6 @@ export function ConversationsTable() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user