feat: add admin stats page with token usage
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user