Next.js full-stack analytics dashboard for new-api. - Direct PostgreSQL readonly queries on logs table - 5 pages: Dashboard, Rankings, Aggregation, Logs, Detail - Dark/Light/System theme with CSS variables - Chinese/English i18n (default Chinese) - Recharts with dual Y-axis for input/output tokens - Lucide icons + Motion animations - Docker + docker-compose with external sinobridge network, port 8019
191 lines
5.3 KiB
TypeScript
191 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { createContext, useContext, useState, useEffect, type ReactNode } from "react";
|
|
|
|
export type Locale = "zh" | "en";
|
|
|
|
const translations = {
|
|
zh: {
|
|
// nav
|
|
"nav.overview": "总览",
|
|
"nav.rankings": "排名",
|
|
"nav.aggregation": "聚合",
|
|
"nav.logs": "日志",
|
|
// common
|
|
"common.loading": "加载中...",
|
|
"common.noData": "暂无数据",
|
|
"common.records": "条记录",
|
|
"common.systemOnline": "系统在线",
|
|
"common.back": "返回",
|
|
"common.backToRankings": "返回排名",
|
|
"common.prevPage": "上一页",
|
|
"common.nextPage": "下一页",
|
|
"common.share": "占比",
|
|
// time range
|
|
"time.today": "今日",
|
|
"time.7d": "7 天",
|
|
"time.30d": "30 天",
|
|
"time.all": "全部",
|
|
// granularity
|
|
"gran.day": "日",
|
|
"gran.week": "周",
|
|
"gran.month": "月",
|
|
// metrics
|
|
"metric.token": "Token",
|
|
"metric.calls": "调用量",
|
|
// dashboard
|
|
"dash.title": "仪表盘",
|
|
"dash.totalCalls": "调用次数",
|
|
"dash.tokenUsage": "Token 消耗",
|
|
"dash.activeUsers": "活跃用户",
|
|
"dash.activeModels": "活跃模型",
|
|
"dash.trend": "使用趋势",
|
|
"dash.userTop10": "用户 Top 10 — Token 消耗",
|
|
"dash.modelTop10": "模型 Top 10 — Token 消耗",
|
|
// table headers
|
|
"th.rank": "#",
|
|
"th.name": "名称",
|
|
"th.user": "用户",
|
|
"th.calls": "调用次数",
|
|
"th.input": "输入",
|
|
"th.output": "输出",
|
|
"th.totalToken": "总 Token",
|
|
"th.time": "时间",
|
|
"th.realModel": "真实模型",
|
|
"th.channel": "渠道",
|
|
"th.latency": "耗时",
|
|
// rankings
|
|
"rank.title": "排名",
|
|
"rank.user": "用户",
|
|
"rank.model": "模型",
|
|
"rank.channel": "渠道",
|
|
// aggregation
|
|
"agg.title": "用户聚合",
|
|
"agg.userCount": "用户数",
|
|
"agg.totalCalls": "总调用",
|
|
"agg.totalToken": "总 Token",
|
|
// logs
|
|
"logs.title": "日志明细",
|
|
"logs.filterUser": "用户名",
|
|
"logs.filterModel": "模型",
|
|
"logs.filterToken": "Token 名称",
|
|
// detail
|
|
"detail.user": "用户",
|
|
"detail.model": "模型",
|
|
"detail.channel": "渠道",
|
|
"detail.trend": "使用趋势",
|
|
"detail.modelDist": "模型分布",
|
|
"detail.userDist": "用户分布",
|
|
// theme
|
|
"theme.light": "浅色",
|
|
"theme.dark": "深色",
|
|
"theme.system": "系统",
|
|
},
|
|
en: {
|
|
"nav.overview": "Overview",
|
|
"nav.rankings": "Rankings",
|
|
"nav.aggregation": "Aggregation",
|
|
"nav.logs": "Logs",
|
|
"common.loading": "Loading...",
|
|
"common.noData": "No data",
|
|
"common.records": "records",
|
|
"common.systemOnline": "System Online",
|
|
"common.back": "Back",
|
|
"common.backToRankings": "Back to Rankings",
|
|
"common.prevPage": "Previous",
|
|
"common.nextPage": "Next",
|
|
"common.share": "Share",
|
|
"time.today": "Today",
|
|
"time.7d": "7 Days",
|
|
"time.30d": "30 Days",
|
|
"time.all": "All",
|
|
"gran.day": "Day",
|
|
"gran.week": "Week",
|
|
"gran.month": "Month",
|
|
"metric.token": "Token",
|
|
"metric.calls": "Calls",
|
|
"dash.title": "Dashboard",
|
|
"dash.totalCalls": "Total Calls",
|
|
"dash.tokenUsage": "Token Usage",
|
|
"dash.activeUsers": "Active Users",
|
|
"dash.activeModels": "Active Models",
|
|
"dash.trend": "Usage Trend",
|
|
"dash.userTop10": "User Top 10 — Token Usage",
|
|
"dash.modelTop10": "Model Top 10 — Token Usage",
|
|
"th.rank": "#",
|
|
"th.name": "Name",
|
|
"th.user": "User",
|
|
"th.calls": "Calls",
|
|
"th.input": "Input",
|
|
"th.output": "Output",
|
|
"th.totalToken": "Total Token",
|
|
"th.time": "Time",
|
|
"th.realModel": "Real Model",
|
|
"th.channel": "Channel",
|
|
"th.latency": "Latency",
|
|
"rank.title": "Rankings",
|
|
"rank.user": "User",
|
|
"rank.model": "Model",
|
|
"rank.channel": "Channel",
|
|
"agg.title": "User Aggregation",
|
|
"agg.userCount": "Users",
|
|
"agg.totalCalls": "Total Calls",
|
|
"agg.totalToken": "Total Token",
|
|
"logs.title": "Log Details",
|
|
"logs.filterUser": "Username",
|
|
"logs.filterModel": "Model",
|
|
"logs.filterToken": "Token Name",
|
|
"detail.user": "User",
|
|
"detail.model": "Model",
|
|
"detail.channel": "Channel",
|
|
"detail.trend": "Usage Trend",
|
|
"detail.modelDist": "Model Distribution",
|
|
"detail.userDist": "User Distribution",
|
|
"theme.light": "Light",
|
|
"theme.dark": "Dark",
|
|
"theme.system": "System",
|
|
},
|
|
} as const;
|
|
|
|
type TranslationKey = keyof typeof translations.zh;
|
|
|
|
interface I18nContextType {
|
|
locale: Locale;
|
|
setLocale: (l: Locale) => void;
|
|
t: (key: TranslationKey) => string;
|
|
}
|
|
|
|
const I18nContext = createContext<I18nContextType>({
|
|
locale: "zh",
|
|
setLocale: () => {},
|
|
t: (key) => key,
|
|
});
|
|
|
|
export function I18nProvider({ children }: { children: ReactNode }) {
|
|
const [locale, setLocale] = useState<Locale>("zh");
|
|
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem("locale") as Locale | null;
|
|
if (saved && (saved === "zh" || saved === "en")) setLocale(saved);
|
|
}, []);
|
|
|
|
const handleSetLocale = (l: Locale) => {
|
|
setLocale(l);
|
|
localStorage.setItem("locale", l);
|
|
};
|
|
|
|
const t = (key: TranslationKey): string => {
|
|
return translations[locale][key] || key;
|
|
};
|
|
|
|
return (
|
|
<I18nContext.Provider value={{ locale, setLocale: handleSetLocale, t }}>
|
|
{children}
|
|
</I18nContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useI18n() {
|
|
return useContext(I18nContext);
|
|
}
|