feat: add admin stats page with token usage

This commit is contained in:
Alexej Wolff
2026-02-11 01:00:02 +01:00
parent 6616c2e9a7
commit c404b1e17c
6 changed files with 455 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { getAdminStats } from "../services/api";
import "./AdminPage.css";
interface StoryStats {
id: string;
title: string;
messageCount: number;
tokens: number;
lastPlayed: string | null;
}
interface AdminStats {
totalStories: number;
totalSessions: number;
totalTokens: number;
stories: StoryStats[];
}
export default function AdminPage() {
const navigate = useNavigate();
const { isAuthenticated, isLoading: authLoading } = useAuth();
const [stats, setStats] = useState<AdminStats | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!authLoading && !isAuthenticated) {
navigate("/");
return;
}
const loadStats = async () => {
if (!isAuthenticated) return;
setIsLoading(true);
try {
const data = await getAdminStats();
if (data) {
setStats(data);
} else {
setError("Не удалось загрузить статистику");
}
} catch (err) {
setError("Ошибка загрузки данных");
} finally {
setIsLoading(false);
}
};
loadStats();
}, [isAuthenticated, authLoading, navigate]);
const formatTokens = (tokens: number) => {
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(2)}M`;
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`;
return tokens.toString();
};
const formatDate = (dateStr: string | null) => {
if (!dateStr) return "Никогда";
return new Date(dateStr).toLocaleDateString("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
if (authLoading || isLoading) {
return (
<div className="admin-page">
<div className="admin-loading">Загрузка статистики...</div>
</div>
);
}
if (error) {
return (
<div className="admin-page">
<div className="admin-error">{error}</div>
</div>
);
}
if (!stats) {
return (
<div className="admin-page">
<div className="admin-error">Нет данных</div>
</div>
);
}
return (
<div className="admin-page">
<h1>📊 Статистика</h1>
<div className="stats-overview">
<div className="stat-card">
<span className="stat-icon">📚</span>
<div className="stat-info">
<span className="stat-value">{stats.totalStories}</span>
<span className="stat-label">Историй</span>
</div>
</div>
<div className="stat-card">
<span className="stat-icon">🎮</span>
<div className="stat-info">
<span className="stat-value">{stats.totalSessions}</span>
<span className="stat-label">Сессий</span>
</div>
</div>
<div className="stat-card highlight">
<span className="stat-icon">🔤</span>
<div className="stat-info">
<span className="stat-value">{formatTokens(stats.totalTokens)}</span>
<span className="stat-label">Всего токенов</span>
</div>
</div>
</div>
<h2>Статистика по историям</h2>
<div className="stories-table-wrapper">
<table className="stories-table">
<thead>
<tr>
<th>История</th>
<th>Сообщений</th>
<th>Токенов</th>
<th>Последняя игра</th>
</tr>
</thead>
<tbody>
{stats.stories.length === 0 ? (
<tr>
<td colSpan={4} className="no-data">Нет историй</td>
</tr>
) : (
stats.stories.map((story) => (
<tr key={story.id}>
<td className="story-title">{story.title}</td>
<td>{story.messageCount}</td>
<td className="tokens-cell">{formatTokens(story.tokens)}</td>
<td className="date-cell">{formatDate(story.lastPlayed)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
<div className="token-info">
<p>
💡 <strong>Примечание:</strong> Токены рассчитаны приблизительно (1 токен 3 символа для русского текста).
Реальное потребление может отличаться.
</p>
</div>
</div>
);
}