first commit
This commit is contained in:
304
components/collections/messages-table.tsx
Normal file
304
components/collections/messages-table.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useCollection } from "@/hooks/useCollection";
|
||||
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 { ChevronLeft, ChevronRight, User, Bot } from "lucide-react";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import {
|
||||
LibreChatMessage,
|
||||
LibreChatUser,
|
||||
LibreChatConversation,
|
||||
} from "@/lib/types";
|
||||
|
||||
// Définir des interfaces pour les types de contenu
|
||||
interface MessageContentItem {
|
||||
text?: string;
|
||||
content?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface MessagePart {
|
||||
text?: string;
|
||||
content?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface MessageWithParts extends LibreChatMessage {
|
||||
parts?: MessagePart[];
|
||||
content?: MessageContentItem[] | string;
|
||||
}
|
||||
|
||||
export function MessagesTable() {
|
||||
const [page, setPage] = useState(1);
|
||||
const limit = 20;
|
||||
|
||||
// Charger les messages
|
||||
const {
|
||||
data: messages = [],
|
||||
total = 0,
|
||||
loading: messagesLoading,
|
||||
} = useCollection<LibreChatMessage>("messages", {
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
|
||||
// Charger les utilisateurs pour les noms
|
||||
const { data: users = [] } = useCollection<LibreChatUser>("users", {
|
||||
limit: 1000,
|
||||
});
|
||||
|
||||
// Charger les conversations pour les titres
|
||||
const { data: conversations = [] } = useCollection<LibreChatConversation>(
|
||||
"conversations",
|
||||
{
|
||||
limit: 1000,
|
||||
}
|
||||
);
|
||||
|
||||
// Créer des maps pour les lookups
|
||||
const userMap = useMemo(() => {
|
||||
return new Map(users.map((user) => [user._id, user]));
|
||||
}, [users]);
|
||||
|
||||
const conversationMap = useMemo(() => {
|
||||
return new Map(
|
||||
conversations.map((conv) => [conv.conversationId || conv._id, conv])
|
||||
);
|
||||
}, [conversations]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
const handlePrevPage = () => {
|
||||
setPage((prev) => Math.max(1, prev - 1));
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
setPage((prev) => Math.min(totalPages, prev + 1));
|
||||
};
|
||||
|
||||
// Fonction pour extraire le contenu du message
|
||||
const getMessageContent = (message: LibreChatMessage): string => {
|
||||
try {
|
||||
// Vérifier le champ text principal
|
||||
if (message.text && typeof message.text === "string") {
|
||||
return message.text.trim();
|
||||
}
|
||||
|
||||
// Traiter le message comme ayant potentiellement des parties
|
||||
const messageWithParts = message as MessageWithParts;
|
||||
|
||||
// Vérifier le champ content (peut être un array ou string)
|
||||
if (messageWithParts.content) {
|
||||
if (typeof messageWithParts.content === "string") {
|
||||
return messageWithParts.content.trim();
|
||||
}
|
||||
if (Array.isArray(messageWithParts.content)) {
|
||||
// Extraire le texte des objets content
|
||||
const textContent = messageWithParts.content
|
||||
.map((item: MessageContentItem | string) => {
|
||||
if (typeof item === "string") return item;
|
||||
if (item && typeof item === "object" && item.text)
|
||||
return item.text;
|
||||
if (item && typeof item === "object" && item.content)
|
||||
return item.content;
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
if (textContent.trim()) return textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les propriétés alternatives
|
||||
if (messageWithParts.parts && Array.isArray(messageWithParts.parts)) {
|
||||
const textContent = messageWithParts.parts
|
||||
.map((part: MessagePart) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part && typeof part === "object" && part.text) return part.text;
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
if (textContent.trim()) return textContent.trim();
|
||||
}
|
||||
|
||||
return "Contenu non disponible";
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de l'extraction du contenu:", error);
|
||||
return "Erreur de lecture du contenu";
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour obtenir le nom d'utilisateur
|
||||
const getUserName = (userId: string): string => {
|
||||
if (!userId || userId === "undefined") return "Utilisateur inconnu";
|
||||
const user = userMap.get(userId);
|
||||
return user?.name || user?.email || `Utilisateur ${userId.slice(-8)}`;
|
||||
};
|
||||
|
||||
// Fonction pour obtenir le titre de la conversation
|
||||
const getConversationTitle = (conversationId: string): string => {
|
||||
if (!conversationId || conversationId === "undefined")
|
||||
return "Conversation inconnue";
|
||||
|
||||
const conversation = conversationMap.get(conversationId);
|
||||
if (conversation && conversation.title) {
|
||||
return conversation.title;
|
||||
}
|
||||
return `Conversation ${conversationId.slice(-6)}`;
|
||||
};
|
||||
|
||||
if (messagesLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Messages</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="h-16 bg-muted animate-pulse rounded" />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Messages récents ({total})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Conversation</TableHead>
|
||||
<TableHead>Utilisateur</TableHead>
|
||||
<TableHead>Rôle</TableHead>
|
||||
<TableHead>Contenu</TableHead>
|
||||
<TableHead>Tokens</TableHead>
|
||||
<TableHead>Créé le</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{messages.map((message) => {
|
||||
const content = getMessageContent(message);
|
||||
const userName = getUserName(message.user);
|
||||
const conversationTitle = getConversationTitle(
|
||||
message.conversationId
|
||||
);
|
||||
const isUser = message.isCreatedByUser;
|
||||
|
||||
return (
|
||||
<TableRow key={message._id}>
|
||||
<TableCell>
|
||||
<span className="font-mono text-xs">
|
||||
{message._id.slice(-8)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-xs">
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
{message.conversationId?.slice(-8) || "N/A"}
|
||||
</span>
|
||||
<div className="text-sm truncate">
|
||||
{conversationTitle}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-xs">
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
{message.user?.slice(-8) || "N/A"}
|
||||
</span>
|
||||
<div className="text-sm truncate">{userName}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={isUser ? "default" : "secondary"}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{isUser ? (
|
||||
<User className="h-3 w-3" />
|
||||
) : (
|
||||
<Bot className="h-3 w-3" />
|
||||
)}
|
||||
{isUser ? "Utilisateur" : "Assistant"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-md">
|
||||
<p className="text-sm truncate">
|
||||
{content.length > 100
|
||||
? `${content.substring(0, 100)}...`
|
||||
: content}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{message.tokenCount > 0 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{message.tokenCount}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatDate(new Date(message.createdAt))}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between space-x-2 py-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Page {page} sur {totalPages} ({total} éléments au total)
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handlePrevPage}
|
||||
disabled={page <= 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Précédent
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleNextPage}
|
||||
disabled={page >= totalPages}
|
||||
>
|
||||
Suivant
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user