TuringToken/web/src/hooks/dashboard/useDashboardData.js

286 lines
8.5 KiB
JavaScript

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { API, isAdmin, showError } from '../../helpers';
const PAGE_SIZE = 200;
const isSuccess = (l) =>
l?.type === 0 || l?.status === 0 || l?.type === 'success';
const sum = (arr, key) =>
arr.reduce((s, l) => s + (Number(l[key]) || 0), 0);
export const useDashboardData = (userState) => {
const initialized = useRef(false);
const [loading, setLoading] = useState(false);
const [recentLogs, setRecentLogs] = useState([]);
const [uptimeData, setUptimeData] = useState([]);
const [uptimeLoading, setUptimeLoading] = useState(false);
const isAdminUser = isAdmin();
const user = userState?.user;
const remaining = Number(user?.quota || 0);
const used = Number(user?.used_quota || 0);
const total = remaining + used;
const loadRecentLogs = useCallback(async () => {
setLoading(true);
try {
const now = Math.floor(Date.now() / 1000);
const start = now - 30 * 86400;
const url = isAdminUser
? `/api/log/?p=0&page_size=${PAGE_SIZE}&start_timestamp=${start}&end_timestamp=${now}`
: `/api/log/self/?p=0&page_size=${PAGE_SIZE}&start_timestamp=${start}&end_timestamp=${now}`;
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setRecentLogs(data?.items || []);
} else {
showError(message);
}
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
}, [isAdminUser]);
const loadUptimeData = useCallback(async () => {
setUptimeLoading(true);
try {
const res = await API.get('/api/uptime/status');
const { success, message, data } = res.data;
if (success) {
setUptimeData(data || []);
} else {
showError(message);
}
} catch (err) {
console.error(err);
} finally {
setUptimeLoading(false);
}
}, []);
useEffect(() => {
if (!initialized.current) {
loadRecentLogs();
loadUptimeData();
initialized.current = true;
}
}, [loadRecentLogs, loadUptimeData]);
// ========== Today / month metrics ==========
const metrics = useMemo(() => {
const now = new Date();
const startOfDay = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
).getTime() / 1000;
const startOfMonth = new Date(
now.getFullYear(),
now.getMonth(),
1,
).getTime() / 1000;
const todayLogs = recentLogs.filter(
(l) => (l?.created_at || 0) >= startOfDay && isSuccess(l),
);
const monthLogs = recentLogs.filter(
(l) => (l?.created_at || 0) >= startOfMonth && isSuccess(l),
);
const todayLatencies = todayLogs
.map((l) => Number(l?.use_time) || 0)
.filter((v) => v > 0);
return {
today: {
requests: todayLogs.length,
tokens: sum(todayLogs, 'token_used'),
cost: sum(todayLogs, 'quota'),
avgLatency:
todayLatencies.length > 0
? Math.round(
todayLatencies.reduce((s, v) => s + v, 0) /
todayLatencies.length,
)
: null,
},
month: {
requests: monthLogs.length,
tokens: sum(monthLogs, 'token_used'),
cost: sum(monthLogs, 'quota'),
},
};
}, [recentLogs]);
// ========== N-day daily stats (for sparkline) ==========
const dailyStats = useMemo(() => {
const now = new Date();
const days = [];
const n = 7;
for (let i = n - 1; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i);
const dayStart = Math.floor(d.getTime() / 1000);
const dayEnd = dayStart + 86400;
const dayLogs = recentLogs.filter(
(l) => (l?.created_at || 0) >= dayStart && (l?.created_at || 0) < dayEnd && isSuccess(l),
);
days.push({
date: d,
count: dayLogs.length,
cost: sum(dayLogs, 'quota'),
tokens: sum(dayLogs, 'token_used'),
});
}
return days;
}, [recentLogs]);
// ========== MoM delta (this month cost vs last month) ==========
const monthDelta = useMemo(() => {
const now = new Date();
const startOfLastMonth = new Date(
now.getFullYear(),
now.getMonth() - 1,
1,
).getTime() / 1000;
const startOfThisMonth = new Date(
now.getFullYear(),
now.getMonth(),
1,
).getTime() / 1000;
const lastMonthLogs = recentLogs.filter(
(l) =>
(l?.created_at || 0) >= startOfLastMonth &&
(l?.created_at || 0) < startOfThisMonth &&
isSuccess(l),
);
const lastCost = sum(lastMonthLogs, 'quota');
if (lastCost === 0) return null;
return ((metrics.month.cost - lastCost) / lastCost) * 100;
}, [recentLogs, metrics.month.cost]);
// ========== Monthly forecast (projected end-of-month cost) ==========
const monthlyForecast = useMemo(() => {
const now = new Date();
const daysInMonth = new Date(
now.getFullYear(),
now.getMonth() + 1,
0,
).getDate();
const daysElapsed = now.getDate();
const spent = metrics.month.cost;
const dailyAvg = daysElapsed > 0 ? spent / daysElapsed : 0;
const projected = Math.round(dailyAvg * daysInMonth);
return {
spent,
balance: remaining,
dailyAvg: Math.round(dailyAvg),
daysInMonth,
daysElapsed,
projected,
monthDelta,
};
}, [metrics.month.cost, remaining, monthDelta]);
// ========== Days remaining estimate ==========
const daysRemaining = useMemo(() => {
const now = new Date();
const daysElapsed = now.getDate();
if (metrics.month.cost === 0 || daysElapsed === 0) return null;
const dailyAvg = metrics.month.cost / daysElapsed;
if (dailyAvg === 0) return null;
const days = Math.floor(remaining / dailyAvg);
return days > 0 ? days : 0;
}, [metrics.month.cost, remaining]);
// ========== SLA overview (uptime / error rate / p95 latency) ==========
const sla = useMemo(() => {
const totalCalls = recentLogs.length;
const successCalls = recentLogs.filter((l) => isSuccess(l)).length;
const errorRate =
totalCalls > 0 ? ((totalCalls - successCalls) / totalCalls) * 100 : null;
const latencies = recentLogs
.map((l) => Number(l?.use_time) || 0)
.filter((v) => v > 0)
.sort((a, b) => a - b);
const p95Latency =
latencies.length > 0
? latencies[Math.min(latencies.length - 1, Math.floor(latencies.length * 0.95))]
: null;
const healthyChannels = uptimeData.filter((c) => c?.status === 1).length;
const uptime =
uptimeData.length > 0
? uptimeData.reduce((s, c) => s + (Number(c?.uptime) || 0), 0) /
uptimeData.length
: null;
const uptimePct =
uptime != null ? uptime * 100 : healthyChannels / Math.max(uptimeData.length, 1) * 100;
return {
uptime: uptime != null ? uptimePct : null,
errorRate,
p95Latency,
totalCalls,
channelCount: uptimeData.length,
healthyChannels,
};
}, [recentLogs, uptimeData]);
// ========== Top models ==========
const topModels = useMemo(() => {
const m = new Map();
recentLogs.forEach((l) => {
const name = l?.model_name || '未知模型';
m.set(name, (m.get(name) || 0) + 1);
});
return Array.from(m.entries())
.map(([name, count]) => ({ name, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 5);
}, [recentLogs]);
const refresh = useCallback(async () => {
await Promise.all([loadRecentLogs(), loadUptimeData()]);
}, [loadRecentLogs, loadUptimeData]);
return {
loading,
recentLogs,
uptimeData,
uptimeLoading,
metrics,
dailyStats,
monthDelta,
daysRemaining,
topModels,
sla,
monthlyForecast,
refresh,
quotaTotal: total,
};
};