new
This commit is contained in:
188
components/dashboard/add-credits.tsx
Normal file
188
components/dashboard/add-credits.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Plus, DollarSign, Users, TrendingUp } from "lucide-react";
|
||||
|
||||
interface AddCreditsStats {
|
||||
totalUsers: number;
|
||||
totalBalances: number;
|
||||
totalCredits: number;
|
||||
averageCredits: number;
|
||||
usersWithoutBalance: number;
|
||||
}
|
||||
|
||||
interface AddCreditsResult {
|
||||
totalUsers: number;
|
||||
updatedBalances: number;
|
||||
createdBalances: number;
|
||||
creditsPerUser: number;
|
||||
totalCreditsAdded: number;
|
||||
}
|
||||
|
||||
export default function AddCredits() {
|
||||
const [stats, setStats] = useState<AddCreditsStats | null>(null);
|
||||
const [result, setResult] = useState<AddCreditsResult | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [analyzing, setAnalyzing] = useState(false);
|
||||
|
||||
const analyzeCurrentCredits = async () => {
|
||||
setAnalyzing(true);
|
||||
try {
|
||||
const response = await fetch("/api/add-credits");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.statistics) {
|
||||
setStats(data.statistics);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de l'analyse:", error);
|
||||
} finally {
|
||||
setAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addCreditsToAllUsers = async () => {
|
||||
if (!confirm("Êtes-vous sûr de vouloir ajouter 5 millions de crédits à TOUS les utilisateurs ? Cette action est irréversible.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/add-credits", {
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setResult(data.statistics);
|
||||
// Rafraîchir les stats
|
||||
await analyzeCurrentCredits();
|
||||
} else {
|
||||
alert("Erreur: " + (data.error || data.message));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de l'ajout des crédits:", error);
|
||||
alert("Erreur lors de l'ajout des crédits");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Plus className="h-5 w-5" />
|
||||
Ajouter des Crédits
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Ajouter 5 millions de tokens à tous les utilisateurs existants
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Bouton d'analyse */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={analyzeCurrentCredits}
|
||||
disabled={analyzing}
|
||||
variant="outline"
|
||||
>
|
||||
{analyzing ? "Analyse..." : "Analyser les crédits actuels"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Statistiques actuelles */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-blue-50 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4 text-blue-600" />
|
||||
<span className="text-sm font-medium">Utilisateurs</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-blue-600">{stats.totalUsers}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="h-4 w-4 text-green-600" />
|
||||
<span className="text-sm font-medium">Total Crédits</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
{stats.totalCredits.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4 text-purple-600" />
|
||||
<span className="text-sm font-medium">Moyenne</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-purple-600">
|
||||
{stats.averageCredits.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4 text-orange-600" />
|
||||
<span className="text-sm font-medium">Sans Balance</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-orange-600">{stats.usersWithoutBalance}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bouton d'ajout de crédits */}
|
||||
{stats && (
|
||||
<div className="border-t pt-4">
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-semibold text-yellow-800 mb-2">⚠️ Action Importante</h4>
|
||||
<p className="text-yellow-700 text-sm">
|
||||
Cette action va ajouter <strong>5,000,000 crédits</strong> à chacun des {stats.totalUsers} utilisateurs.
|
||||
<br />
|
||||
Total de crédits qui seront ajoutés: <strong>{(stats.totalUsers * 5000000).toLocaleString()}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={addCreditsToAllUsers}
|
||||
disabled={loading}
|
||||
className="w-full bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{loading ? "Ajout en cours..." : `Ajouter 5M crédits à ${stats.totalUsers} utilisateurs`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Résultats */}
|
||||
{result && (
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="font-semibold text-green-600 mb-3">✅ Crédits ajoutés avec succès !</h4>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600">Balances mises à jour:</span>
|
||||
<Badge variant="secondary" className="ml-2">{result.updatedBalances}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Nouvelles balances:</span>
|
||||
<Badge variant="secondary" className="ml-2">{result.createdBalances}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Crédits par utilisateur:</span>
|
||||
<Badge variant="secondary" className="ml-2">{result.creditsPerUser.toLocaleString()}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Total ajouté:</span>
|
||||
<Badge variant="secondary" className="ml-2">{result.totalCreditsAdded.toLocaleString()}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -9,14 +9,23 @@ import {
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
} from "recharts";
|
||||
|
||||
interface ModelDistributionChartProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
data: Array<{
|
||||
name: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
models?: Array<{
|
||||
name: string;
|
||||
value: number;
|
||||
}>;
|
||||
}>;
|
||||
showLegend?: boolean;
|
||||
totalTokens?: number;
|
||||
}
|
||||
|
||||
interface TooltipPayload {
|
||||
@@ -24,6 +33,11 @@ interface TooltipPayload {
|
||||
payload: {
|
||||
name: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
models?: Array<{
|
||||
name: string;
|
||||
value: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,63 +46,211 @@ interface CustomTooltipProps {
|
||||
payload?: TooltipPayload[];
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div style={{
|
||||
backgroundColor: "hsl(var(--background))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "8px",
|
||||
padding: "8px",
|
||||
fontSize: "12px"
|
||||
}}>
|
||||
<p style={{ margin: 0, color: "#ff0000" }}>
|
||||
{`${payload[0].value.toLocaleString()} tokens`}
|
||||
</p>
|
||||
<p style={{ margin: 0, color: "#ff0000" }}>
|
||||
{payload[0].payload.name}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Couleurs par fournisseur selon l'image
|
||||
const providerColors: { [key: string]: string } = {
|
||||
Anthropic: "#7C3AED", // Violet vif
|
||||
OpenAI: "#059669", // Vert turquoise vif
|
||||
"Mistral AI": "#D97706", // Orange vif
|
||||
Meta: "#DB2777", // Rose/Magenta vif
|
||||
Google: "#2563EB", // Bleu vif
|
||||
Cohere: "#0891B2", // Cyan vif
|
||||
};
|
||||
|
||||
// Fonction pour regrouper les modèles par fournisseur
|
||||
const groupByProvider = (modelData: Array<{ name: string; value: number }>) => {
|
||||
const providerMap: {
|
||||
[key: string]: {
|
||||
value: number;
|
||||
models: Array<{ name: string; value: number }>;
|
||||
};
|
||||
} = {};
|
||||
|
||||
modelData.forEach((model) => {
|
||||
let provider = "";
|
||||
|
||||
// Déterminer le fournisseur basé sur le nom du modèle
|
||||
if (
|
||||
model.name.toLowerCase().includes("claude") ||
|
||||
model.name.toLowerCase().includes("anthropic")
|
||||
) {
|
||||
provider = "Anthropic";
|
||||
} else if (
|
||||
model.name.toLowerCase().includes("gpt") ||
|
||||
model.name.toLowerCase().includes("openai")
|
||||
) {
|
||||
provider = "OpenAI";
|
||||
} else if (model.name.toLowerCase().includes("mistral")) {
|
||||
provider = "Mistral AI";
|
||||
} else if (
|
||||
model.name.toLowerCase().includes("llama") ||
|
||||
model.name.toLowerCase().includes("meta")
|
||||
) {
|
||||
provider = "Meta";
|
||||
} else if (
|
||||
model.name.toLowerCase().includes("palm") ||
|
||||
model.name.toLowerCase().includes("gemini") ||
|
||||
model.name.toLowerCase().includes("google")
|
||||
) {
|
||||
provider = "Google";
|
||||
} else if (model.name.toLowerCase().includes("cohere")) {
|
||||
provider = "Cohere";
|
||||
} else {
|
||||
provider = "Autres";
|
||||
}
|
||||
|
||||
if (!providerMap[provider]) {
|
||||
providerMap[provider] = { value: 0, models: [] };
|
||||
}
|
||||
|
||||
providerMap[provider].value += model.value;
|
||||
providerMap[provider].models.push(model);
|
||||
});
|
||||
|
||||
return Object.entries(providerMap).map(([name, data]) => ({
|
||||
name,
|
||||
value: data.value,
|
||||
models: data.models,
|
||||
color: providerColors[name] || "#6B7280",
|
||||
}));
|
||||
};
|
||||
|
||||
const CustomTooltip = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export function ModelDistributionChart({
|
||||
title,
|
||||
subtitle,
|
||||
data,
|
||||
totalTokens,
|
||||
}: 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
|
||||
})) || []
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{title}
|
||||
</CardTitle>
|
||||
{subtitle && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{subtitle}</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<BarChart data={data}>
|
||||
<BarChart
|
||||
data={groupedData}
|
||||
margin={{ top: 10, right: 10, left: 10, bottom: 10 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tick={false}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
interval={0}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={(value) => {
|
||||
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
|
||||
if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
|
||||
return value.toString();
|
||||
}}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="#000000"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar dataKey="value" radius={[2, 2, 0, 0]}>
|
||||
{groupedData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
{/* Petites cartes légères pour chaque provider */}
|
||||
<div className="mt-4 grid grid-cols-2 gap-3">
|
||||
{groupedData.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-3 rounded-lg border border-muted/20 bg-muted/5 hover:bg-muted/10 transition-colors"
|
||||
style={{
|
||||
borderLeftColor: item.color,
|
||||
borderLeftWidth: "3px",
|
||||
borderLeftStyle: "solid",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: item.color }}
|
||||
></div>
|
||||
<h3
|
||||
className="text-sm font-medium"
|
||||
style={{ color: item.color }}
|
||||
>
|
||||
{item.name}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">tokens</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Total général */}
|
||||
{totalTokens && (
|
||||
<div className="mt-4 pt-3 border-t border-muted/20 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Total général:{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
{totalTokens.toLocaleString()}
|
||||
</span>{" "}
|
||||
tokens
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Légende dynamique des modèles */}
|
||||
{allModels.length > 0 && (
|
||||
<div className="mt-4 pt-3 border-t border-muted/20">
|
||||
<h4 className="text-sm font-medium text-muted-foreground mb-3 text-center">
|
||||
Modèles utilisés
|
||||
</h4>
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2">
|
||||
{allModels
|
||||
.sort((a, b) => b.value - a.value) // Trier par usage décroissant
|
||||
.map((model, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: model.color }}
|
||||
></div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{model.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -44,12 +44,12 @@ export function RealUserActivityChart() {
|
||||
{
|
||||
name: "Utilisateurs actifs",
|
||||
value: activity.activeUsers,
|
||||
color: "#22c55e", // Vert clair pour actifs
|
||||
color: "#000000", // Noir pour actifs
|
||||
},
|
||||
{
|
||||
name: "Utilisateurs inactifs",
|
||||
value: activity.inactiveUsers,
|
||||
color: "#ef4444", // Rouge pour inactifs
|
||||
color: "#666666", // Gris pour inactifs
|
||||
},
|
||||
];
|
||||
|
||||
@@ -62,7 +62,7 @@ export function RealUserActivityChart() {
|
||||
Activité des utilisateurs
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Actifs = connectés dans les 7 derniers jours
|
||||
Actifs = connectés dans les 30 derniers jours
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -74,8 +74,10 @@ export function RealUserActivityChart() {
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
paddingAngle={5}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
stroke="#ffffff"
|
||||
strokeWidth={2}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
@@ -86,17 +88,20 @@ export function RealUserActivityChart() {
|
||||
backgroundColor: "hsl(var(--background))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "8px",
|
||||
fontSize: "12px"
|
||||
}}
|
||||
formatter={(value: number) => [
|
||||
`${value} utilisateurs (${((value / total) * 100).toFixed(
|
||||
1
|
||||
)}%)`,
|
||||
`${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`,
|
||||
"",
|
||||
]}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{
|
||||
paddingTop: "20px",
|
||||
fontSize: "12px"
|
||||
}}
|
||||
formatter={(value, entry) => (
|
||||
<span style={{ color: entry.color }}>
|
||||
<span style={{ color: entry.color, fontWeight: 500 }}>
|
||||
{value}: {entry.payload?.value} (
|
||||
{((entry.payload?.value / total) * 100).toFixed(1)}%)
|
||||
</span>
|
||||
|
||||
@@ -31,8 +31,8 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
|
||||
<AreaChart data={data}>
|
||||
<defs>
|
||||
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={color} stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor={color} stopOpacity={0}/>
|
||||
<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" />
|
||||
@@ -46,6 +46,11 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={(value) => {
|
||||
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
|
||||
if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
|
||||
return value.toString();
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
@@ -54,12 +59,16 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
formatter={(value: number) => [
|
||||
value >= 1000 ? `${(value / 1000).toFixed(1)}K tokens` : `${value} tokens`,
|
||||
'Tokens consommés'
|
||||
]}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
strokeWidth={3}
|
||||
fill="url(#colorGradient)"
|
||||
/>
|
||||
</AreaChart>
|
||||
|
||||
@@ -20,12 +20,12 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
|
||||
{
|
||||
name: 'Utilisateurs actifs',
|
||||
value: activeUsers,
|
||||
color: '#22c55e' // Vert clair pour actifs
|
||||
color: '#000000' // Noir pour actifs
|
||||
},
|
||||
{
|
||||
name: 'Utilisateurs inactifs',
|
||||
value: inactiveUsers,
|
||||
color: '#ef4444' // Rouge pour inactifs
|
||||
color: '#666666' // Gris pour inactifs
|
||||
},
|
||||
];
|
||||
|
||||
@@ -36,7 +36,7 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium">Activité des utilisateurs</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Actifs = connectés dans les 7 derniers jours
|
||||
Actifs = connectés dans les 30 derniers jours
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -48,8 +48,10 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
paddingAngle={5}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
stroke="#ffffff"
|
||||
strokeWidth={2}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
@@ -60,16 +62,22 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
|
||||
backgroundColor: 'hsl(var(--background))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
formatter={(value: number) => [
|
||||
`${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`,
|
||||
''
|
||||
'',
|
||||
]}
|
||||
/>
|
||||
<Legend
|
||||
<Legend
|
||||
wrapperStyle={{
|
||||
paddingTop: "20px",
|
||||
fontSize: "12px"
|
||||
}}
|
||||
formatter={(value, entry) => (
|
||||
<span style={{ color: entry.color }}>
|
||||
{value}: {entry.payload?.value} ({((entry.payload?.value / total) * 100).toFixed(1)}%)
|
||||
<span style={{ color: entry.color, fontWeight: 500 }}>
|
||||
{value}: {entry.payload?.value} (
|
||||
{((entry.payload?.value / total) * 100).toFixed(1)}%)
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,7 @@ export function RealTimeStats() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-6">
|
||||
<div className="h-64 bg-muted animate-pulse rounded-lg" />
|
||||
<div className="h-64 bg-muted animate-pulse rounded-lg" />
|
||||
</div>
|
||||
@@ -20,7 +20,7 @@ export function RealTimeStats() {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
@@ -47,7 +47,7 @@ export function RealTimeStats() {
|
||||
|
||||
if (!stats) {
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center h-64">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -67,7 +67,7 @@ export function RealTimeStats() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-6">
|
||||
<SimpleStatsChart
|
||||
title="Tokens consommés par jour"
|
||||
data={stats.dailyTokens}
|
||||
|
||||
@@ -46,24 +46,20 @@ export function UsageAnalytics() {
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// Console log pour débugger les données balances
|
||||
console.log("=== DONNÉES BALANCES RÉCUPÉRÉES ===");
|
||||
console.log("Nombre total d'entrées balances:", balances.length);
|
||||
console.log("Toutes les entrées balances:", balances);
|
||||
|
||||
// NOUVEAU : Console log pour débugger les utilisateurs
|
||||
console.log("=== DONNÉES UTILISATEURS ===");
|
||||
console.log("Nombre total d'utilisateurs:", users.length);
|
||||
console.log("Premiers 5 utilisateurs:", users.slice(0, 5));
|
||||
|
||||
// Analyser les doublons
|
||||
console.log("=== CALCUL DES STATISTIQUES ===");
|
||||
console.log("Utilisateurs:", users.length);
|
||||
console.log("Conversations:", conversations.length);
|
||||
console.log("Transactions:", transactions.length);
|
||||
console.log("Balances:", balances.length);
|
||||
|
||||
// Analyser les doublons dans les balances
|
||||
const userCounts = new Map<string, number>();
|
||||
balances.forEach(balance => {
|
||||
balances.forEach((balance) => {
|
||||
const userId = balance.user;
|
||||
userCounts.set(userId, (userCounts.get(userId) || 0) + 1);
|
||||
});
|
||||
|
||||
const duplicateUsers = Array.from(userCounts.entries()).filter(([_, count]) => count > 1);
|
||||
|
||||
const duplicateUsers = Array.from(userCounts.entries()).filter(([, count]) => count > 1);
|
||||
console.log("Utilisateurs avec plusieurs entrées:", duplicateUsers);
|
||||
|
||||
// Afficher quelques exemples d'entrées
|
||||
@@ -73,6 +69,29 @@ export function UsageAnalytics() {
|
||||
const totalBrut = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0);
|
||||
console.log("Total brut (avec doublons potentiels):", totalBrut);
|
||||
|
||||
// Ajouter des logs détaillés pour comprendre le problème
|
||||
console.log("=== DIAGNOSTIC DÉTAILLÉ ===");
|
||||
|
||||
// Analyser les doublons
|
||||
const duplicateDetails = Array.from(userCounts.entries())
|
||||
.filter(([, count]) => count > 1)
|
||||
.map(([userId, count]) => {
|
||||
const userBalances = balances.filter(b => b.user === userId);
|
||||
const totalCredits = userBalances.reduce((sum, b) => sum + (b.tokenCredits || 0), 0);
|
||||
return {
|
||||
userId,
|
||||
count,
|
||||
totalCredits,
|
||||
balances: userBalances.map(b => ({
|
||||
credits: b.tokenCredits,
|
||||
createdAt: b.createdAt,
|
||||
updatedAt: b.updatedAt
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
console.log("Détails des doublons:", duplicateDetails);
|
||||
|
||||
// NOUVEAU : Identifier les utilisateurs fantômes
|
||||
console.log("=== ANALYSE DES UTILISATEURS FANTÔMES ===");
|
||||
const userIds = new Set(users.map(user => user._id));
|
||||
@@ -90,41 +109,58 @@ export function UsageAnalytics() {
|
||||
|
||||
console.log("Crédits des utilisateurs fantômes:", phantomCredits);
|
||||
console.log("Crédits des vrais utilisateurs:", totalBrut - phantomCredits);
|
||||
|
||||
// Analyser les utilisateurs fantômes
|
||||
const phantomDetails = uniquePhantomUsers.map(userId => {
|
||||
const userBalances = balances.filter(b => b.user === userId);
|
||||
const totalCredits = userBalances.reduce((sum, b) => sum + (b.tokenCredits || 0), 0);
|
||||
return { userId, totalCredits, count: userBalances.length };
|
||||
});
|
||||
|
||||
console.log("Détails des utilisateurs fantômes:", phantomDetails);
|
||||
|
||||
// Calculer les utilisateurs actifs (5 dernières minutes)
|
||||
const fiveMinutesAgo = new Date();
|
||||
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5);
|
||||
// Calculer les utilisateurs actifs (30 derniers jours)
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
const activeUsers = users.filter((user) => {
|
||||
const lastActivity = new Date(user.updatedAt || user.createdAt);
|
||||
return lastActivity >= fiveMinutesAgo;
|
||||
return lastActivity >= thirtyDaysAgo;
|
||||
}).length;
|
||||
|
||||
// CORRECTION : Créer une map des crédits par utilisateur en évitant les doublons
|
||||
// CORRECTION AMÉLIORÉE : Créer une map des crédits par utilisateur
|
||||
const creditsMap = new Map<string, number>();
|
||||
|
||||
// Grouper les balances par utilisateur
|
||||
const balancesByUser = new Map<string, LibreChatBalance[]>();
|
||||
balances.forEach((balance) => {
|
||||
const userId = balance.user;
|
||||
if (!balancesByUser.has(userId)) {
|
||||
balancesByUser.set(userId, []);
|
||||
// Ignorer les utilisateurs fantômes (qui n'existent plus)
|
||||
if (users.some(user => user._id === userId)) {
|
||||
if (!balancesByUser.has(userId)) {
|
||||
balancesByUser.set(userId, []);
|
||||
}
|
||||
balancesByUser.get(userId)!.push(balance);
|
||||
}
|
||||
balancesByUser.get(userId)!.push(balance);
|
||||
});
|
||||
|
||||
// Pour chaque utilisateur, prendre seulement la dernière entrée
|
||||
// Pour chaque utilisateur, calculer les crédits selon votre logique métier
|
||||
balancesByUser.forEach((userBalances, userId) => {
|
||||
if (userBalances.length > 0) {
|
||||
// Trier par date de mise à jour (plus récent en premier)
|
||||
// OPTION A: Prendre la balance la plus récente
|
||||
const sortedBalances = userBalances.sort((a, b) => {
|
||||
const aDate = new Date((a.updatedAt as string) || (a.createdAt as string) || 0);
|
||||
const bDate = new Date((b.updatedAt as string) || (b.createdAt as string) || 0);
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
creditsMap.set(userId, sortedBalances[0].tokenCredits || 0);
|
||||
|
||||
// Prendre la plus récente
|
||||
const latestBalance = sortedBalances[0];
|
||||
creditsMap.set(userId, latestBalance.tokenCredits || 0);
|
||||
// OPTION B: Sommer toutes les balances (si c'est votre logique)
|
||||
// const totalCredits = userBalances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0);
|
||||
// creditsMap.set(userId, totalCredits);
|
||||
|
||||
// OPTION C: Prendre la balance avec le plus de crédits
|
||||
// const maxCredits = Math.max(...userBalances.map(b => b.tokenCredits || 0));
|
||||
// creditsMap.set(userId, maxCredits);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -167,12 +203,16 @@ export function UsageAnalytics() {
|
||||
}
|
||||
});
|
||||
|
||||
// CORRECTION : Calculer le total des crédits depuis la map corrigée
|
||||
// Calculer le total des crédits depuis la map corrigée (sans doublons ni fantômes)
|
||||
const totalCreditsUsed = Array.from(creditsMap.values()).reduce(
|
||||
(sum, credits) => sum + credits,
|
||||
0
|
||||
);
|
||||
|
||||
console.log("=== RÉSULTATS CORRIGÉS ===");
|
||||
console.log("Crédits totaux (sans doublons ni fantômes):", totalCreditsUsed);
|
||||
console.log("Utilisateurs avec crédits:", creditsMap.size);
|
||||
|
||||
// Tous les utilisateurs triés par tokens puis conversations
|
||||
const allUsers = Array.from(userStats.entries())
|
||||
.map(([userId, stats]) => ({
|
||||
@@ -242,7 +282,7 @@ export function UsageAnalytics() {
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.totalUsers}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{stats.activeUsers} actifs cette semaine
|
||||
{stats.activeUsers} actifs ce mois
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user