applicatif 3M, user, chart
This commit is contained in:
@@ -8,6 +8,12 @@ export async function GET() {
|
||||
// Récupérer toutes les transactions
|
||||
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}`);
|
||||
|
||||
// Vérifier les champs de date disponibles dans les transactions
|
||||
@@ -106,14 +112,75 @@ export async function GET() {
|
||||
.map(([name, value]) => ({ name, 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:", {
|
||||
dailyStats,
|
||||
dailyConnections,
|
||||
totalModels: modelData.length,
|
||||
topModel: modelData[0]
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
dailyTokens: dailyStats,
|
||||
dailyConnections: dailyConnections,
|
||||
modelDistribution: modelData
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -158,7 +158,7 @@ export default function AddCredits() {
|
||||
⚠️ Action Importante
|
||||
</h4>
|
||||
<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.
|
||||
<br />
|
||||
Total de crédits qui seront ajoutés:{" "}
|
||||
@@ -173,7 +173,7 @@ export default function AddCredits() {
|
||||
>
|
||||
{loading
|
||||
? "Ajout en cours..."
|
||||
: `Ajouter 5M crédits à ${stats.totalUsers} utilisateurs`}
|
||||
: `Ajouter 3M crédits à ${stats.totalUsers} utilisateurs`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
80
components/dashboard/charts/user-connections-chart.tsx
Normal file
80
components/dashboard/charts/user-connections-chart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -52,17 +52,17 @@ export function DashboardUsersList() {
|
||||
|
||||
// Récupérer toutes les données nécessaires
|
||||
const { data: users, loading: usersLoading } =
|
||||
useCollection<LibreChatUser>("users");
|
||||
useCollection<LibreChatUser>("users", { limit: 1000 });
|
||||
const { data: conversations, loading: conversationsLoading } =
|
||||
useCollection<LibreChatConversation>("conversations");
|
||||
useCollection<LibreChatConversation>("conversations", { limit: 1000 });
|
||||
const { data: balances, loading: balancesLoading } =
|
||||
useCollection<LibreChatBalance>("balances");
|
||||
useCollection<LibreChatBalance>("balances", { limit: 1000 });
|
||||
const { data: messages, loading: messagesLoading } =
|
||||
useCollection<LibreChatMessage>("messages");
|
||||
useCollection<LibreChatMessage>("messages", { limit: 1000 });
|
||||
const { data: tokens, loading: tokensLoading } =
|
||||
useCollection<TokenDocument>("tokens");
|
||||
useCollection<TokenDocument>("tokens", { limit: 1000 });
|
||||
const { data: toolcalls, loading: toolcallsLoading } =
|
||||
useCollection<ToolcallDocument>("toolcalls");
|
||||
useCollection<ToolcallDocument>("toolcalls", { limit: 1000 });
|
||||
|
||||
const processUsers = useCallback(() => {
|
||||
if (
|
||||
@@ -174,20 +174,23 @@ export function DashboardUsersList() {
|
||||
const latestBalance = sortedBalances[0];
|
||||
const credits = latestBalance ? latestBalance.tokenCredits || 0 : 0;
|
||||
|
||||
// Calculer les tokens consommés depuis les crédits
|
||||
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)
|
||||
// Calculer les tokens réellement consommés depuis les messages (approche principale)
|
||||
const totalTokens = Math.max(
|
||||
totalTokensFromMessages,
|
||||
totalTokensFromConversations,
|
||||
tokensFromTokensCollection,
|
||||
tokensFromToolcalls,
|
||||
tokensFromCredits
|
||||
tokensFromToolcalls
|
||||
);
|
||||
|
||||
// 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é
|
||||
console.log(`👤 User ${user.name || user.email}:`, {
|
||||
conversations: userConversations.length,
|
||||
@@ -200,7 +203,8 @@ export function DashboardUsersList() {
|
||||
currentCredits: credits,
|
||||
creditsUsed: creditsUsed,
|
||||
tokensFromCredits: tokensFromCredits,
|
||||
finalTokens: totalTokens,
|
||||
finalTokens: finalTokens,
|
||||
willBeIncluded: finalTokens > 0,
|
||||
messagesSample: userMessages.slice(0, 2).map((m) => ({
|
||||
tokenCount: m.tokenCount,
|
||||
model: m.model,
|
||||
@@ -213,14 +217,14 @@ export function DashboardUsersList() {
|
||||
})),
|
||||
});
|
||||
|
||||
// Ajouter l'utilisateur seulement s'il a des données significatives
|
||||
if (userConversations.length > 0 || totalTokens > 0 || credits > 0) {
|
||||
// Ajouter l'utilisateur s'il a consommé des tokens (éviter les faux positifs de 3M tokens)
|
||||
if (finalTokens > 0 && finalTokens < INITIAL_CREDITS) {
|
||||
processedUsers.push({
|
||||
userId: user._id,
|
||||
userName:
|
||||
user.name || user.username || user.email || "Utilisateur inconnu",
|
||||
conversations: userConversations.length,
|
||||
tokens: totalTokens,
|
||||
tokens: finalTokens,
|
||||
credits: credits,
|
||||
});
|
||||
}
|
||||
@@ -231,6 +235,17 @@ export function DashboardUsersList() {
|
||||
.sort((a, b) => b.tokens - a.tokens)
|
||||
.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);
|
||||
setTopUsers(sortedUsers);
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
import { useMetrics } from "@/hooks/useMetrics";
|
||||
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";
|
||||
|
||||
export function OverviewMetrics() {
|
||||
@@ -30,62 +38,63 @@ export function OverviewMetrics() {
|
||||
const creditsInEuros = convertCreditsToEuros(metrics.totalCredits);
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<MetricCard
|
||||
title="Utilisateurs totaux"
|
||||
value={metrics.totalUsers}
|
||||
icon={Users}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Utilisateurs actifs"
|
||||
value={metrics.activeUsers}
|
||||
icon={UserCheck}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Administrateurs"
|
||||
value={metrics.totalAdmins}
|
||||
icon={Shield}
|
||||
/>
|
||||
|
||||
{/* Nouvelle carte pour les tokens consommés */}
|
||||
<MetricCard
|
||||
title="Tokens consommés"
|
||||
value={metrics.totalTokensConsumed?.toLocaleString() || "0"}
|
||||
icon={Activity}
|
||||
description={`${Math.round((metrics.totalTokensConsumed || 0) / (metrics.totalUsers || 1))} par utilisateur`}
|
||||
/>
|
||||
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<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 className="space-y-4">
|
||||
{/* Ligne 1: Utilisateurs actifs, Conversations actives, Tokens consommés */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<MetricCard
|
||||
title="Utilisateurs actifs"
|
||||
value={metrics.activeUsers}
|
||||
icon={UserCheck}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Conversations actives"
|
||||
value={metrics.activeConversations}
|
||||
icon={MessageSquare}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Tokens consommés"
|
||||
value={metrics.totalTokensConsumed?.toLocaleString() || "0"}
|
||||
icon={Activity}
|
||||
description={`${Math.round(
|
||||
(metrics.totalTokensConsumed || 0) / (metrics.totalUsers || 1)
|
||||
)} par utilisateur`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Ligne 2: Utilisateurs totaux, Messages totaux, Crédits totaux */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<MetricCard
|
||||
title="Utilisateurs totaux"
|
||||
value={metrics.totalUsers}
|
||||
icon={Users}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Messages totaux"
|
||||
value={metrics.totalMessages}
|
||||
icon={FileText}
|
||||
/>
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<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 className="text-2xl font-bold mb-1">
|
||||
{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 className="text-2xl font-bold mb-1">
|
||||
{metrics.totalCredits.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-green-600">
|
||||
{creditsInEuros.formatted.usd} USD
|
||||
<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 className="text-xs text-green-600">
|
||||
{creditsInEuros.formatted.usd} USD
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MetricCard
|
||||
title="Conversations actives"
|
||||
value={metrics.activeConversations}
|
||||
icon={MessageSquare}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Messages totaux"
|
||||
value={metrics.totalMessages}
|
||||
icon={FileText}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useStats } from "@/hooks/useStats";
|
||||
import { SimpleStatsChart } from "./charts/simple-stats-chart";
|
||||
import { UserConnectionsChart } from "./charts/user-connections-chart";
|
||||
import { ModelDistributionChart } from "./charts/model-distribution-chart";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
export function RealTimeStats() {
|
||||
const { stats, loading, error } = useStats();
|
||||
|
||||
console.log("RealTimeStats - stats:", stats);
|
||||
console.log("RealTimeStats - loading:", loading);
|
||||
console.log("RealTimeStats - error:", error);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -67,16 +73,35 @@ export function RealTimeStats() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div 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}
|
||||
/>
|
||||
</div>
|
||||
<Tabs defaultValue="connections" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="connections">Nombre de connexions par utilisateur/par jour</TabsTrigger>
|
||||
<TabsTrigger value="tokens">Tokens consommés par jour</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="connections" className="space-y-6">
|
||||
<UserConnectionsChart
|
||||
title="Nombre de connexions par utilisateur/par jour"
|
||||
data={stats.dailyConnections || []}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ interface DailyToken {
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface DailyConnection {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface ModelDistribution {
|
||||
name: string;
|
||||
value: number;
|
||||
@@ -14,6 +19,7 @@ interface ModelDistribution {
|
||||
|
||||
interface StatsData {
|
||||
dailyTokens: DailyToken[];
|
||||
dailyConnections: DailyConnection[];
|
||||
modelDistribution: ModelDistribution[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user