305 lines
9.6 KiB
TypeScript
305 lines
9.6 KiB
TypeScript
"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>
|
|
);
|
|
}
|