diff --git a/app/conversations/page.tsx b/app/conversations/page.tsx index fe58375..a7cc329 100644 --- a/app/conversations/page.tsx +++ b/app/conversations/page.tsx @@ -6,10 +6,10 @@ export default function ConversationsPage() {

Conversations

- Gestion des conversations Cercle GPTTT + Gestion des conversations Cercle GPTT

- + ); diff --git a/app/page.tsx b/app/page.tsx index 4e1970a..f932698 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"; import { OverviewMetrics } from "@/components/dashboard/overview-metrics"; import { RealTimeStats } from "@/components/dashboard/real-time-stats"; import { RealUserActivityChart } from "@/components/dashboard/charts/real-user-activity-chart"; +import { DashboardUsersList } from "@/components/dashboard/dashboard-users-list"; import { Users, MessageSquare, @@ -28,7 +29,7 @@ export default function Dashboard() {

- {/* Métriques principales */} + {/* Métriques principales - maintenant avec tokens consommés */} } > @@ -49,21 +50,29 @@ export default function Dashboard() { - {/* Grille pour activité utilisateurs et actions */} -
- {/* Activité des utilisateurs avec vraies données */} -
- - } - > - - -
+ {/* Grille pour activité utilisateurs et top utilisateurs */} +
+ {/* Activité des utilisateurs */} + + } + > + + - {/* Actions rapides épurées */} -
+ {/* Top 5 utilisateurs - nouveau composant */} + + } + > + + +
+ + {/* Actions rapides */} +
@@ -139,7 +148,6 @@ export default function Dashboard() { -
); diff --git a/components/dashboard/charts/model-distribution-chart.tsx b/components/dashboard/charts/model-distribution-chart.tsx index 7b7b132..69298bc 100644 --- a/components/dashboard/charts/model-distribution-chart.tsx +++ b/components/dashboard/charts/model-distribution-chart.tsx @@ -9,7 +9,6 @@ import { CartesianGrid, Tooltip, ResponsiveContainer, - Cell, } from "recharts"; interface ModelDistributionChartProps { @@ -28,20 +27,27 @@ interface ModelDistributionChartProps { totalTokens?: number; } -interface TooltipPayload { +interface ModelData { + name: string; value: number; - payload: { - name: string; - value: number; - color?: string; - models?: Array<{ - name: string; - value: number; - }>; - }; } -// Couleurs par fournisseur selon l'image +interface StackedDataEntry { + provider: string; + total: number; + models: ModelData[]; + baseColor: string; + modelColors: string[]; + [key: string]: string | number | ModelData[] | string[]; +} + +interface ModelInfo { + color: string; + name: string; + provider: string; +} + +// Couleurs par fournisseur (couleurs de base) const providerColors: { [key: string]: string } = { Anthropic: "#7C3AED", // Violet vif OpenAI: "#059669", // Vert turquoise vif @@ -51,8 +57,25 @@ const providerColors: { [key: string]: string } = { Cohere: "#0891B2", // Cyan vif }; -// Fonction pour regrouper les modèles par fournisseur -const groupByProvider = (modelData: Array<{ name: string; value: number }>) => { +// Fonction pour générer des variations de couleur pour les modèles d'un même provider +const generateModelColors = (baseColor: string, modelCount: number) => { + const colors = []; + for (let i = 0; i < modelCount; i++) { + // Créer des variations en ajustant la luminosité + const opacity = 1 - i * 0.15; // De 1.0 à 0.4 environ + colors.push( + `${baseColor}${Math.round(opacity * 255) + .toString(16) + .padStart(2, "0")}` + ); + } + return colors; +}; + +// Fonction pour regrouper les modèles par fournisseur et préparer les données pour le graphique empilé +const prepareStackedData = ( + modelData: Array<{ name: string; value: number }> +) => { const providerMap: { [key: string]: { value: number; @@ -101,40 +124,89 @@ const groupByProvider = (modelData: Array<{ name: string; value: number }>) => { providerMap[provider].models.push(model); }); - return Object.entries(providerMap).map(([name, data]) => ({ - name, - value: data.value, - models: data.models, - color: providerColors[name] || "#6B7280", - })); + // Créer les données pour le graphique empilé + const stackedData: StackedDataEntry[] = Object.entries(providerMap).map( + ([providerName, data]) => { + const baseColor = providerColors[providerName] || "#6B7280"; + const modelColors = generateModelColors(baseColor, data.models.length); + + // Créer un objet avec le provider comme clé et chaque modèle comme propriété + const stackedEntry: StackedDataEntry = { + provider: providerName, + total: data.value, + models: data.models, + baseColor, + modelColors, + }; + + // Ajouter chaque modèle comme propriété séparée pour le stacking + data.models.forEach((model, index) => { + const modelKey = `${providerName}_${model.name}`; + stackedEntry[modelKey] = model.value; + stackedEntry[`${modelKey}_color`] = modelColors[index]; + stackedEntry[`${modelKey}_name`] = model.name; + }); + + return stackedEntry; + } + ); + + // Créer la liste de tous les modèles uniques pour les barres + const allModelKeys: string[] = []; + const modelInfo: { [key: string]: ModelInfo } = {}; + + stackedData.forEach((entry) => { + entry.models.forEach((model, index) => { + const modelKey = `${entry.provider}_${model.name}`; + allModelKeys.push(modelKey); + modelInfo[modelKey] = { + color: entry.modelColors[index], + name: model.name, + provider: entry.provider, + }; + }); + }); + + return { stackedData, allModelKeys, modelInfo }; }; -const CustomTooltip = ({ +interface CustomStackedTooltipProps { + active?: boolean; + payload?: Array<{ + payload: StackedDataEntry; + }>; + label?: string; +} + +const CustomStackedTooltip = ({ active, payload, -}: { - active?: boolean; - payload?: TooltipPayload[]; -}) => { + label, +}: CustomStackedTooltipProps) => { if (active && payload && payload.length) { - const data = payload[0].payload; + const providerData = payload[0].payload; + const totalTokens = providerData.total; + return (
-

{data.name}

-

Tokens: {data.value.toLocaleString()}

- {data.models && data.models.length > 0 && ( +

{label}

+

+ Total: {totalTokens.toLocaleString()} tokens +

+ {providerData.models && (

Modèles:

- {data.models.slice(0, 5).map((model, index) => ( -

- • {model.name}: {model.value.toLocaleString()} -

+ {providerData.models.map((model, index) => ( +
+
+

+ {model.name}: {model.value.toLocaleString()} tokens +

+
))} - {data.models.length > 5 && ( -

- ... et {data.models.length - 5} autres -

- )}
)}
@@ -151,17 +223,11 @@ export function ModelDistributionChart({ }: ModelDistributionChartProps) { // Si les données sont déjà groupées par fournisseur, les utiliser directement // Sinon, les regrouper automatiquement - const groupedData = data[0]?.models ? data : groupByProvider(data); - - // Créer une liste de tous les modèles avec leurs couleurs - const allModels = groupedData.flatMap( - (provider) => - provider.models?.map((model) => ({ - name: model.name, - color: provider.color, - value: model.value, - })) || [] - ); + const modelData = data[0]?.models + ? data.flatMap((d) => d.models || []) + : data; + const { stackedData, allModelKeys, modelInfo } = + prepareStackedData(modelData); return ( @@ -174,20 +240,20 @@ export function ModelDistributionChart({ )} - + - } /> - - {groupedData.map((entry, index) => ( - - ))} - + } /> + + {/* Créer une barre empilée pour chaque modèle */} + {allModelKeys.map((modelKey) => ( + + ))} - {/* Petites cartes légères pour chaque provider */} + {/* Légende des providers avec leurs couleurs de base */}
- {groupedData.map((item, index) => ( + {stackedData.map((item, index) => (

- {item.name} + {item.provider}

- {item.value.toLocaleString()} + {item.total.toLocaleString()} +

+

+ {item.models.length} modèle{item.models.length > 1 ? "s" : ""}

-

tokens

))}
@@ -254,29 +328,37 @@ export function ModelDistributionChart({ )} - {/* Légende dynamique des modèles */} - {allModels.length > 0 && ( -
-

- Modèles utilisés -

-
- {allModels - .sort((a, b) => b.value - a.value) // Trier par usage décroissant - .map((model, index) => ( -
-
- - {model.name} - -
- ))} -
+ {/* Légende détaillée des modèles avec leurs couleurs spécifiques */} +
+

+ Modèles par provider +

+
+ {stackedData.map((provider) => ( +
+
+ {provider.provider} +
+
+ {provider.models.map((model, index) => ( +
+
+ + {model.name} ({model.value.toLocaleString()}) + +
+ ))} +
+
+ ))}
- )} +
); diff --git a/components/dashboard/dashboard-users-list.tsx b/components/dashboard/dashboard-users-list.tsx new file mode 100644 index 0000000..12c2b36 --- /dev/null +++ b/components/dashboard/dashboard-users-list.tsx @@ -0,0 +1,237 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Users } from "lucide-react"; +import Link from "next/link"; +import { useCollection } from "@/hooks/useCollection"; +import { + LibreChatUser, + LibreChatConversation, + LibreChatTransaction, + LibreChatBalance, +} from "@/lib/types"; + +interface DashboardUser { + userId: string; + userName: string; + conversations: number; + tokens: number; + credits: number; +} + +// Fonction utilitaire pour valider et convertir les dates +const isValidDate = (value: unknown): value is string | number | Date => { + if (!value) return false; + if (value instanceof Date) return !isNaN(value.getTime()); + if (typeof value === 'string' || typeof value === 'number') { + const date = new Date(value); + return !isNaN(date.getTime()); + } + return false; +}; + +export function DashboardUsersList() { + const [topUsers, setTopUsers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const { data: users, loading: usersLoading } = useCollection('users'); + const { data: conversations, loading: conversationsLoading } = useCollection('conversations'); + const { data: transactions, loading: transactionsLoading } = useCollection('transactions'); + const { data: balances, loading: balancesLoading } = useCollection('balances'); + + const processUsers = useCallback(() => { + if (!users?.length || !conversations?.length || !transactions?.length || !balances?.length) { + return; + } + + console.log("🔄 Processing users data..."); + console.log("Users:", users.length); + console.log("Conversations:", conversations.length); + console.log("Transactions:", transactions.length); + console.log("Balances:", balances.length); + + const processedUsers: DashboardUser[] = []; + + users.forEach((user: LibreChatUser) => { + // Obtenir les conversations de l'utilisateur + const userConversations = conversations.filter( + (conv: LibreChatConversation) => conv.user === user._id + ); + + // Obtenir les transactions de l'utilisateur + const userTransactions = transactions.filter( + (trans: LibreChatTransaction) => trans.user === user._id + ); + + // Calculer les tokens consommés + const totalTokens = userTransactions.reduce( + (sum: number, trans: LibreChatTransaction) => sum + (trans.rawAmount || 0), + 0 + ); + + // Obtenir les balances de l'utilisateur + const userBalances = balances.filter( + (balance: LibreChatBalance) => balance.user === user._id + ); + + // Trier par date de mise à jour (plus récent en premier) + const sortedBalances = userBalances.sort((a, b) => { + const dateA = a.updatedAt || a.createdAt; + const dateB = b.updatedAt || b.createdAt; + + // Vérifier que les dates sont valides avant de les comparer + if (isValidDate(dateA) && isValidDate(dateB)) { + return new Date(dateB as string | number | Date).getTime() - new Date(dateA as string | number | Date).getTime(); + } + + // Si seulement une date existe et est valide, la privilégier + if (isValidDate(dateA) && !isValidDate(dateB)) return -1; + if (!isValidDate(dateA) && isValidDate(dateB)) return 1; + + // Si aucune date n'existe ou n'est valide, garder l'ordre actuel + return 0; + }); + + // Prendre la balance la plus récente + const latestBalance = sortedBalances[0]; + const credits = latestBalance ? latestBalance.tokenCredits || 0 : 0; + + // Ajouter l'utilisateur seulement s'il a des données significatives + if (userConversations.length > 0 || totalTokens > 0 || credits > 0) { + processedUsers.push({ + userId: user._id, + userName: user.name || user.username || user.email || 'Utilisateur inconnu', + conversations: userConversations.length, + tokens: totalTokens, + credits: credits, + }); + } + }); + + // Trier par tokens consommés (décroissant) et prendre les 5 premiers + const sortedUsers = processedUsers + .sort((a, b) => b.tokens - a.tokens) + .slice(0, 5); + + console.log("✅ Top 5 users processed:", sortedUsers); + setTopUsers(sortedUsers); + setIsLoading(false); + }, [users, conversations, transactions, balances]); + + useEffect(() => { + const allDataLoaded = !usersLoading && !conversationsLoading && !transactionsLoading && !balancesLoading; + + if (allDataLoaded) { + processUsers(); + } else { + setIsLoading(true); + } + }, [usersLoading, conversationsLoading, transactionsLoading, balancesLoading, processUsers]); + + if (isLoading) { + return ( + + + + + Top 5 utilisateurs + + + + +
+ {[...Array(5)].map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+
+
+ ); + } + + if (topUsers.length === 0) { + return ( + + + + + Top 5 utilisateurs + + + + + + +
+ Aucun utilisateur trouvé +
+
+
+ ); + } + + return ( + + + + + Top 5 utilisateurs + + + + + + +
+ {topUsers.map((user, index) => ( +
+
+ + {index + 1} + +
+

{user.userName}

+

+ {user.conversations} conversation{user.conversations !== 1 ? 's' : ''} +

+
+
+
+

+ {user.tokens.toLocaleString()} tokens +

+

+ {user.credits.toLocaleString()} crédits +

+
+
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/overview-metrics.tsx b/components/dashboard/overview-metrics.tsx index a7a963b..77c79fa 100644 --- a/components/dashboard/overview-metrics.tsx +++ b/components/dashboard/overview-metrics.tsx @@ -2,7 +2,7 @@ import { useMetrics } from "@/hooks/useMetrics"; import { MetricCard } from "@/components/ui/metric-card"; -import { Users, UserCheck, Shield, Coins, MessageSquare, FileText, Euro } from "lucide-react"; +import { Users, UserCheck, Shield, Coins, MessageSquare, FileText, Euro, Activity } from "lucide-react"; import { convertCreditsToEuros } from "@/lib/utils/currency"; export function OverviewMetrics() { @@ -10,8 +10,8 @@ export function OverviewMetrics() { if (loading) { return ( -
- {Array.from({ length: 6 }).map((_, i) => ( +
+ {Array.from({ length: 7 }).map((_, i) => (
))}
@@ -30,7 +30,7 @@ export function OverviewMetrics() { const creditsInEuros = convertCreditsToEuros(metrics.totalCredits); return ( -
+
+ + {/* Nouvelle carte pour les tokens consommés */} + +

Crédits totaux

diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 861977c..e96efbd 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -19,7 +19,6 @@ import { Bot, ChevronLeft, ChevronRight, - BarChart3, LogOut, User, Mail, @@ -28,15 +27,10 @@ import type { User as SupabaseUser } from "@supabase/supabase-js"; const topLevelNavigation = [ { - name: "Dashboard", + name: "Analytics", href: "/", icon: LayoutDashboard, }, - { - name: "Analytics", - href: "/analytics", - icon: BarChart3, - }, ]; const navigationGroups = [ diff --git a/components/ui/metric-card.tsx b/components/ui/metric-card.tsx index d0762c0..a0136dd 100644 --- a/components/ui/metric-card.tsx +++ b/components/ui/metric-card.tsx @@ -7,6 +7,7 @@ interface MetricCardProps { title: string; value: number | string; icon: LucideIcon; + description?: string; trend?: { value: number; isPositive: boolean; @@ -18,6 +19,7 @@ export function MetricCard({ title, value, icon: Icon, + description, trend, className }: MetricCardProps) { @@ -33,6 +35,11 @@ export function MetricCard({
{typeof value === 'number' ? formatNumber(value) : value}
+ {description && ( +

+ {description} +

+ )} {trend && (