applicatif 3M, user, chart

This commit is contained in:
nBiqoz
2025-10-31 20:51:39 +01:00
parent 0efe96f4e2
commit 1c7bca8e35
7 changed files with 286 additions and 84 deletions

View File

@@ -8,6 +8,12 @@ export async function GET() {
// Récupérer toutes les transactions // Récupérer toutes les transactions
const transactions = await db.collection("transactions").find({}).toArray(); const transactions = await db.collection("transactions").find({}).toArray();
// Récupérer les conversations pour analyser les connexions utilisateurs
const conversations = await db.collection("conversations").find({}).toArray();
// Récupérer les messages pour une analyse plus précise de l'activité
const messages = await db.collection("messages").find({}).toArray();
console.log(`Total transactions trouvées: ${transactions.length}`); console.log(`Total transactions trouvées: ${transactions.length}`);
// Vérifier les champs de date disponibles dans les transactions // Vérifier les champs de date disponibles dans les transactions
@@ -106,14 +112,75 @@ export async function GET() {
.map(([name, value]) => ({ name, value })) .map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value); .sort((a, b) => b.value - a.value);
// Calculer les connexions utilisateurs par jour (7 derniers jours)
const dailyConnections = [];
for (let i = 6; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
date.setHours(0, 0, 0, 0);
const nextDate = new Date(date);
nextDate.setDate(nextDate.getDate() + 1);
// Analyser l'activité des utilisateurs via les messages
const activeUsers = new Set();
messages.forEach(message => {
let messageDate = null;
if (message.createdAt) {
messageDate = new Date(message.createdAt);
} else if (message.updatedAt) {
messageDate = new Date(message.updatedAt);
} else if (message._id && message._id.getTimestamp) {
messageDate = message._id.getTimestamp();
}
if (messageDate && messageDate >= date && messageDate < nextDate) {
if (message.user && message.isCreatedByUser) {
activeUsers.add(message.user);
}
}
});
// Aussi analyser via les conversations créées ce jour-là
conversations.forEach(conversation => {
let convDate = null;
if (conversation.createdAt) {
convDate = new Date(conversation.createdAt);
} else if (conversation.updatedAt) {
convDate = new Date(conversation.updatedAt);
} else if (conversation._id && conversation._id.getTimestamp) {
convDate = conversation._id.getTimestamp();
}
if (convDate && convDate >= date && convDate < nextDate) {
if (conversation.user) {
activeUsers.add(conversation.user);
}
}
});
console.log(`${dayNames[date.getDay()]} (${date.toISOString().split('T')[0]}): ${activeUsers.size} utilisateurs actifs`);
dailyConnections.push({
name: dayNames[date.getDay()],
value: activeUsers.size
});
}
console.log("Statistiques calculées:", { console.log("Statistiques calculées:", {
dailyStats, dailyStats,
dailyConnections,
totalModels: modelData.length, totalModels: modelData.length,
topModel: modelData[0] topModel: modelData[0]
}); });
return NextResponse.json({ return NextResponse.json({
dailyTokens: dailyStats, dailyTokens: dailyStats,
dailyConnections: dailyConnections,
modelDistribution: modelData modelDistribution: modelData
}); });
} catch (error) { } catch (error) {

View File

@@ -158,7 +158,7 @@ export default function AddCredits() {
Action Importante Action Importante
</h4> </h4>
<p className="text-yellow-700 text-sm"> <p className="text-yellow-700 text-sm">
Cette action va ajouter <strong>5,000,000 crédits</strong> à Cette action va ajouter <strong>3,000,000 crédits</strong> à
chacun des {stats.totalUsers} utilisateurs. chacun des {stats.totalUsers} utilisateurs.
<br /> <br />
Total de crédits qui seront ajoutés:{" "} Total de crédits qui seront ajoutés:{" "}
@@ -173,7 +173,7 @@ export default function AddCredits() {
> >
{loading {loading
? "Ajout en cours..." ? "Ajout en cours..."
: `Ajouter 5M crédits à ${stats.totalUsers} utilisateurs`} : `Ajouter 3M crédits à ${stats.totalUsers} utilisateurs`}
</Button> </Button>
</div> </div>
)} )}

View File

@@ -0,0 +1,80 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
} from "recharts";
interface UserConnectionsChartProps {
title: string;
data: Array<{
name: string;
value: number;
}>;
color?: string;
}
export function UserConnectionsChart({ title, data, color = "hsl(var(--chart-2))" }: UserConnectionsChartProps) {
console.log("UserConnectionsChart - data:", data);
console.log("UserConnectionsChart - title:", title);
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{title}</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={data}>
<defs>
<linearGradient id="connectionsGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.8}/>
<stop offset="95%" stopColor={color} stopOpacity={0.2}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
className="text-xs fill-muted-foreground"
/>
<YAxis
axisLine={false}
tickLine={false}
className="text-xs fill-muted-foreground"
tickFormatter={(value) => {
return value.toString();
}}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--background))',
border: '1px solid hsl(var(--border))',
borderRadius: '8px',
fontSize: '12px'
}}
formatter={(value: number) => [
`${value} utilisateur${value > 1 ? 's' : ''}`,
'Connexions'
]}
/>
<Area
type="monotone"
dataKey="value"
stroke={color}
strokeWidth={3}
fill="url(#connectionsGradient)"
/>
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
}

View File

@@ -52,17 +52,17 @@ export function DashboardUsersList() {
// Récupérer toutes les données nécessaires // Récupérer toutes les données nécessaires
const { data: users, loading: usersLoading } = const { data: users, loading: usersLoading } =
useCollection<LibreChatUser>("users"); useCollection<LibreChatUser>("users", { limit: 1000 });
const { data: conversations, loading: conversationsLoading } = const { data: conversations, loading: conversationsLoading } =
useCollection<LibreChatConversation>("conversations"); useCollection<LibreChatConversation>("conversations", { limit: 1000 });
const { data: balances, loading: balancesLoading } = const { data: balances, loading: balancesLoading } =
useCollection<LibreChatBalance>("balances"); useCollection<LibreChatBalance>("balances", { limit: 1000 });
const { data: messages, loading: messagesLoading } = const { data: messages, loading: messagesLoading } =
useCollection<LibreChatMessage>("messages"); useCollection<LibreChatMessage>("messages", { limit: 1000 });
const { data: tokens, loading: tokensLoading } = const { data: tokens, loading: tokensLoading } =
useCollection<TokenDocument>("tokens"); useCollection<TokenDocument>("tokens", { limit: 1000 });
const { data: toolcalls, loading: toolcallsLoading } = const { data: toolcalls, loading: toolcallsLoading } =
useCollection<ToolcallDocument>("toolcalls"); useCollection<ToolcallDocument>("toolcalls", { limit: 1000 });
const processUsers = useCallback(() => { const processUsers = useCallback(() => {
if ( if (
@@ -174,20 +174,23 @@ export function DashboardUsersList() {
const latestBalance = sortedBalances[0]; const latestBalance = sortedBalances[0];
const credits = latestBalance ? latestBalance.tokenCredits || 0 : 0; const credits = latestBalance ? latestBalance.tokenCredits || 0 : 0;
// Calculer les tokens consommés depuis les crédits // Calculer les tokens réellement consommés depuis les messages (approche principale)
const INITIAL_CREDITS = 3000000;
const creditsUsed = INITIAL_CREDITS - credits;
const tokensFromCredits = creditsUsed > 0 ? creditsUsed : 0;
// Prendre la valeur la plus élevée (plus précise)
const totalTokens = Math.max( const totalTokens = Math.max(
totalTokensFromMessages, totalTokensFromMessages,
totalTokensFromConversations, totalTokensFromConversations,
tokensFromTokensCollection, tokensFromTokensCollection,
tokensFromToolcalls, tokensFromToolcalls
tokensFromCredits
); );
// Calculer les tokens depuis les crédits seulement si on n'a pas de données de messages
const INITIAL_CREDITS = 3000000;
const creditsUsed = INITIAL_CREDITS - credits;
const tokensFromCredits = creditsUsed > 0 ? creditsUsed : 0;
// Si on n'a pas de tokens depuis les messages mais qu'on a une consommation de crédits significative
const finalTokens = totalTokens > 0 ? totalTokens :
(tokensFromCredits > 0 && tokensFromCredits < INITIAL_CREDITS) ? tokensFromCredits : 0;
// Log de débogage très détaillé // Log de débogage très détaillé
console.log(`👤 User ${user.name || user.email}:`, { console.log(`👤 User ${user.name || user.email}:`, {
conversations: userConversations.length, conversations: userConversations.length,
@@ -200,7 +203,8 @@ export function DashboardUsersList() {
currentCredits: credits, currentCredits: credits,
creditsUsed: creditsUsed, creditsUsed: creditsUsed,
tokensFromCredits: tokensFromCredits, tokensFromCredits: tokensFromCredits,
finalTokens: totalTokens, finalTokens: finalTokens,
willBeIncluded: finalTokens > 0,
messagesSample: userMessages.slice(0, 2).map((m) => ({ messagesSample: userMessages.slice(0, 2).map((m) => ({
tokenCount: m.tokenCount, tokenCount: m.tokenCount,
model: m.model, model: m.model,
@@ -213,14 +217,14 @@ export function DashboardUsersList() {
})), })),
}); });
// Ajouter l'utilisateur seulement s'il a des données significatives // Ajouter l'utilisateur s'il a consommé des tokens (éviter les faux positifs de 3M tokens)
if (userConversations.length > 0 || totalTokens > 0 || credits > 0) { if (finalTokens > 0 && finalTokens < INITIAL_CREDITS) {
processedUsers.push({ processedUsers.push({
userId: user._id, userId: user._id,
userName: userName:
user.name || user.username || user.email || "Utilisateur inconnu", user.name || user.username || user.email || "Utilisateur inconnu",
conversations: userConversations.length, conversations: userConversations.length,
tokens: totalTokens, tokens: finalTokens,
credits: credits, credits: credits,
}); });
} }
@@ -231,6 +235,17 @@ export function DashboardUsersList() {
.sort((a, b) => b.tokens - a.tokens) .sort((a, b) => b.tokens - a.tokens)
.slice(0, 5); .slice(0, 5);
console.log("📊 Processing summary:", {
totalUsers: users.length,
usersWithActivity: processedUsers.length,
top5Users: sortedUsers.length,
allProcessedUsers: processedUsers.map(u => ({
name: u.userName,
conversations: u.conversations,
tokens: u.tokens,
credits: u.credits
}))
});
console.log("✅ Top 5 users processed:", sortedUsers); console.log("✅ Top 5 users processed:", sortedUsers);
setTopUsers(sortedUsers); setTopUsers(sortedUsers);
setIsLoading(false); setIsLoading(false);

View File

@@ -2,7 +2,15 @@
import { useMetrics } from "@/hooks/useMetrics"; import { useMetrics } from "@/hooks/useMetrics";
import { MetricCard } from "@/components/ui/metric-card"; import { MetricCard } from "@/components/ui/metric-card";
import { Users, UserCheck, Shield, Coins, MessageSquare, FileText, Euro, Activity } from "lucide-react"; import {
Users,
UserCheck,
Coins,
MessageSquare,
FileText,
Euro,
Activity,
} from "lucide-react";
import { convertCreditsToEuros } from "@/lib/utils/currency"; import { convertCreditsToEuros } from "@/lib/utils/currency";
export function OverviewMetrics() { export function OverviewMetrics() {
@@ -30,62 +38,63 @@ export function OverviewMetrics() {
const creditsInEuros = convertCreditsToEuros(metrics.totalCredits); const creditsInEuros = convertCreditsToEuros(metrics.totalCredits);
return ( return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="space-y-4">
<MetricCard {/* Ligne 1: Utilisateurs actifs, Conversations actives, Tokens consommés */}
title="Utilisateurs totaux" <div className="grid gap-4 md:grid-cols-3">
value={metrics.totalUsers} <MetricCard
icon={Users} title="Utilisateurs actifs"
/> value={metrics.activeUsers}
<MetricCard icon={UserCheck}
title="Utilisateurs actifs" />
value={metrics.activeUsers} <MetricCard
icon={UserCheck} title="Conversations actives"
/> value={metrics.activeConversations}
<MetricCard icon={MessageSquare}
title="Administrateurs" />
value={metrics.totalAdmins} <MetricCard
icon={Shield} title="Tokens consommés"
/> value={metrics.totalTokensConsumed?.toLocaleString() || "0"}
icon={Activity}
description={`${Math.round(
(metrics.totalTokensConsumed || 0) / (metrics.totalUsers || 1)
)} par utilisateur`}
/>
</div>
{/* Nouvelle carte pour les tokens consommés */} {/* Ligne 2: Utilisateurs totaux, Messages totaux, Crédits totaux */}
<MetricCard <div className="grid gap-4 md:grid-cols-3">
title="Tokens consommés" <MetricCard
value={metrics.totalTokensConsumed?.toLocaleString() || "0"} title="Utilisateurs totaux"
icon={Activity} value={metrics.totalUsers}
description={`${Math.round((metrics.totalTokensConsumed || 0) / (metrics.totalUsers || 1))} par utilisateur`} icon={Users}
/> />
<MetricCard
<div className="bg-white rounded-lg border p-6"> title="Messages totaux"
<div className="flex items-center justify-between mb-2"> value={metrics.totalMessages}
<h3 className="text-sm font-medium text-gray-600">Crédits totaux</h3> icon={FileText}
<div className="flex items-center gap-1"> />
<Coins className="h-4 w-4 text-gray-400" /> <div className="bg-white rounded-lg border p-6">
<Euro className="h-4 w-4 text-green-600" /> <div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-600">Crédits totaux</h3>
<div className="flex items-center gap-1">
<Coins className="h-4 w-4 text-gray-400" />
<Euro className="h-4 w-4 text-green-600" />
</div>
</div> </div>
</div> <div className="text-2xl font-bold mb-1">
<div className="text-2xl font-bold mb-1"> {metrics.totalCredits.toLocaleString()}
{metrics.totalCredits.toLocaleString()}
</div>
<div className="text-sm text-gray-500 mb-2">crédits disponibles</div>
<div className="p-2 bg-green-50 rounded border border-green-200">
<div className="text-sm font-semibold text-green-800">
{creditsInEuros.formatted.eur}
</div> </div>
<div className="text-xs text-green-600"> <div className="text-sm text-gray-500 mb-2">crédits disponibles</div>
{creditsInEuros.formatted.usd} USD <div className="p-2 bg-green-50 rounded border border-green-200">
<div className="text-sm font-semibold text-green-800">
{creditsInEuros.formatted.eur}
</div>
<div className="text-xs text-green-600">
{creditsInEuros.formatted.usd} USD
</div>
</div> </div>
</div> </div>
</div> </div>
<MetricCard
title="Conversations actives"
value={metrics.activeConversations}
icon={MessageSquare}
/>
<MetricCard
title="Messages totaux"
value={metrics.totalMessages}
icon={FileText}
/>
</div> </div>
); );
} }

View File

@@ -1,14 +1,20 @@
"use client"; "use client";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useStats } from "@/hooks/useStats"; import { useStats } from "@/hooks/useStats";
import { SimpleStatsChart } from "./charts/simple-stats-chart"; import { SimpleStatsChart } from "./charts/simple-stats-chart";
import { UserConnectionsChart } from "./charts/user-connections-chart";
import { ModelDistributionChart } from "./charts/model-distribution-chart"; import { ModelDistributionChart } from "./charts/model-distribution-chart";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
export function RealTimeStats() { export function RealTimeStats() {
const { stats, loading, error } = useStats(); const { stats, loading, error } = useStats();
console.log("RealTimeStats - stats:", stats);
console.log("RealTimeStats - loading:", loading);
console.log("RealTimeStats - error:", error);
if (loading) { if (loading) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -67,16 +73,35 @@ export function RealTimeStats() {
} }
return ( return (
<div className="space-y-6"> <Tabs defaultValue="connections" className="space-y-6">
<SimpleStatsChart <TabsList className="grid w-full grid-cols-2">
title="Tokens consommés par jour" <TabsTrigger value="connections">Nombre de connexions par utilisateur/par jour</TabsTrigger>
data={stats.dailyTokens} <TabsTrigger value="tokens">Tokens consommés par jour</TabsTrigger>
color="hsl(var(--primary))" </TabsList>
/>
<ModelDistributionChart <TabsContent value="connections" className="space-y-6">
title="Répartition par modèle" <UserConnectionsChart
data={stats.modelDistribution} title="Nombre de connexions par utilisateur/par jour"
/> data={stats.dailyConnections || []}
</div> color="hsl(var(--chart-2))"
/>
<ModelDistributionChart
title="Répartition par modèle"
data={stats.modelDistribution}
/>
</TabsContent>
<TabsContent value="tokens" className="space-y-6">
<SimpleStatsChart
title="Tokens consommés par jour"
data={stats.dailyTokens}
color="hsl(var(--primary))"
/>
<ModelDistributionChart
title="Répartition par modèle"
data={stats.modelDistribution}
/>
</TabsContent>
</Tabs>
); );
} }

View File

@@ -7,6 +7,11 @@ interface DailyToken {
value: number; value: number;
} }
interface DailyConnection {
name: string;
value: number;
}
interface ModelDistribution { interface ModelDistribution {
name: string; name: string;
value: number; value: number;
@@ -14,6 +19,7 @@ interface ModelDistribution {
interface StatsData { interface StatsData {
dailyTokens: DailyToken[]; dailyTokens: DailyToken[];
dailyConnections: DailyConnection[];
modelDistribution: ModelDistribution[]; modelDistribution: ModelDistribution[];
} }