mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 05:32:19 +03:00
Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { useTranslations } from "next-intl";
|
|
import Card from "../Card";
|
|
import { getModelColor } from "@/shared/constants/colors";
|
|
import { fmtCompact as fmt, fmtCost } from "@/shared/utils/formatting";
|
|
import { ChartLoadingCard, DarkTooltip, useRecharts } from "./rechartsCore";
|
|
|
|
// ── DailyTrendChart (Recharts) ─────────────────────────────────────────────
|
|
|
|
export function DailyTrendChart({ dailyTrend }) {
|
|
const t = useTranslations("analytics");
|
|
const chartData = useMemo(() => {
|
|
return (dailyTrend || []).map((d) => ({
|
|
date: d.date.slice(5),
|
|
[t("chartInput")]: d.promptTokens,
|
|
[t("chartOutput")]: d.completionTokens,
|
|
[t("chartCost")]: d.cost || 0,
|
|
}));
|
|
}, [dailyTrend, t]);
|
|
|
|
const hasCost = useMemo(() => chartData.some((d) => d[t("chartCost")] > 0), [chartData, t]);
|
|
|
|
if (!chartData.length) {
|
|
return (
|
|
<Card className="p-4">
|
|
<h3 className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-3">
|
|
{t("chartModelUsageOverTime")}
|
|
</h3>
|
|
<div className="text-center text-text-muted text-sm py-8">{t("chartNoData")}</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return <DailyTrendChartBody chartData={chartData} hasCost={hasCost} />;
|
|
}
|
|
|
|
function DailyTrendChartBody({ chartData, hasCost }) {
|
|
const t = useTranslations("analytics");
|
|
const recharts = useRecharts();
|
|
|
|
if (!recharts) {
|
|
return <ChartLoadingCard />;
|
|
}
|
|
|
|
const { ComposedChart, Bar, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } = recharts;
|
|
|
|
return (
|
|
<Card className="p-4 flex-1">
|
|
<h3 className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-3">
|
|
{t("chartModelUsageOverTime")}
|
|
</h3>
|
|
<ResponsiveContainer width="100%" height={140}>
|
|
<ComposedChart
|
|
data={chartData}
|
|
margin={{ top: 0, right: hasCost ? 40 : 0, left: 0, bottom: 0 }}
|
|
>
|
|
<XAxis
|
|
dataKey="date"
|
|
tick={{ fontSize: 9, fill: "var(--color-text-muted)" }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
interval={Math.max(Math.floor(chartData.length / 6), 0)}
|
|
/>
|
|
{hasCost && (
|
|
<YAxis
|
|
yAxisId="cost"
|
|
orientation="right"
|
|
tick={{ fontSize: 8, fill: "#f59e0b" }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tickFormatter={(v) => `$${v.toFixed(2)}`}
|
|
width={36}
|
|
/>
|
|
)}
|
|
<Tooltip content={<CostTooltip />} cursor={{ fill: "rgba(255,255,255,0.04)" }} />
|
|
<Bar
|
|
dataKey={t("chartInput")}
|
|
stackId="a"
|
|
fill="var(--color-primary)"
|
|
opacity={0.7}
|
|
radius={[0, 0, 0, 0]}
|
|
animationDuration={600}
|
|
/>
|
|
<Bar
|
|
dataKey={t("chartOutput")}
|
|
stackId="a"
|
|
fill="#10b981"
|
|
opacity={0.7}
|
|
radius={[3, 3, 0, 0]}
|
|
animationDuration={600}
|
|
/>
|
|
{hasCost && (
|
|
<Line
|
|
yAxisId="cost"
|
|
type="monotone"
|
|
dataKey={t("chartCost")}
|
|
stroke="#f59e0b"
|
|
strokeWidth={2}
|
|
dot={false}
|
|
animationDuration={600}
|
|
/>
|
|
)}
|
|
</ComposedChart>
|
|
</ResponsiveContainer>
|
|
<div className="flex items-center gap-4 mt-2 text-[10px] text-text-muted">
|
|
<span className="flex items-center gap-1">
|
|
<span className="w-2 h-2 rounded-full bg-primary/70" /> {t("chartInput")}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<span className="w-2 h-2 rounded-full bg-emerald-500/70" /> {t("chartOutput")}
|
|
</span>
|
|
{hasCost && (
|
|
<span className="flex items-center gap-1">
|
|
<span className="w-2 h-2 rounded-full bg-amber-500/70" /> {t("chartCost")} ($)
|
|
</span>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// ── Cost-aware Tooltip ─────────────────────────────────────────────────────
|
|
|
|
function CostTooltip({
|
|
active,
|
|
payload,
|
|
label,
|
|
}: {
|
|
active?: boolean;
|
|
payload?: any[];
|
|
label?: any;
|
|
}) {
|
|
const t = useTranslations("analytics");
|
|
if (!active || !payload?.length) return null;
|
|
return (
|
|
<div className="rounded-lg border border-white/10 bg-surface px-3 py-2 text-xs shadow-lg">
|
|
{label && <div className="font-semibold text-text-main mb-1">{label}</div>}
|
|
{payload.map((entry, i) => (
|
|
<div key={i} className="flex items-center gap-1.5 text-text-muted">
|
|
<span
|
|
className="w-2 h-2 rounded-full shrink-0"
|
|
style={{ backgroundColor: entry.color }}
|
|
/>
|
|
<span>{entry.name}:</span>
|
|
<span className="font-mono font-medium text-text-main">
|
|
{entry.name === t("chartCost") ? fmtCost(entry.value) : fmt(entry.value)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── WeeklyPattern (Recharts) ───────────────────────────────────────────────
|
|
|
|
function WeeklyPattern({ weeklyPattern }) {
|
|
const t = useTranslations("analytics");
|
|
const chartData = useMemo(() => {
|
|
return (weeklyPattern || []).map((w) => ({
|
|
day: w.day.slice(0, 3),
|
|
Tokens: w.totalTokens,
|
|
}));
|
|
}, [weeklyPattern]);
|
|
|
|
return <WeeklyPatternBody chartData={chartData} title={t("chartWeekly")} />;
|
|
}
|
|
|
|
function WeeklyPatternBody({ chartData, title }) {
|
|
const recharts = useRecharts();
|
|
|
|
if (!recharts) {
|
|
return (
|
|
<Card className="px-4 py-3">
|
|
<h3 className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
|
|
{title}
|
|
</h3>
|
|
<div className="h-12" />
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const { ResponsiveContainer, BarChart, XAxis, Tooltip, Bar } = recharts;
|
|
|
|
return (
|
|
<Card className="px-4 py-3">
|
|
<h3 className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
|
|
{title}
|
|
</h3>
|
|
<ResponsiveContainer width="100%" height={48}>
|
|
<BarChart data={chartData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
|
<XAxis
|
|
dataKey="day"
|
|
tick={{ fontSize: 9, fill: "var(--color-text-muted)" }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
/>
|
|
<Tooltip
|
|
content={<DarkTooltip formatter={fmt} />}
|
|
cursor={{ fill: "rgba(255,255,255,0.04)" }}
|
|
/>
|
|
<Bar
|
|
dataKey="Tokens"
|
|
fill="var(--color-text-muted)"
|
|
opacity={0.3}
|
|
radius={[3, 3, 0, 0]}
|
|
animationDuration={400}
|
|
/>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// ── MostActiveDay7d ────────────────────────────────────────────────────────
|
|
|
|
export function ModelOverTimeChart({ dailyByModel, modelNames }) {
|
|
const t = useTranslations("analytics");
|
|
const data = useMemo(() => dailyByModel || [], [dailyByModel]);
|
|
const models = useMemo(() => modelNames || [], [modelNames]);
|
|
|
|
// Prepare chart data — format dates (must be before early return for rules-of-hooks)
|
|
const chartData = useMemo(() => {
|
|
return data.map((d) => {
|
|
const row = { ...d };
|
|
// Short date label
|
|
if (d.date) {
|
|
const parts = d.date.split("-");
|
|
row.dateLabel = `${parts[1]}/${parts[2]}`;
|
|
}
|
|
return row;
|
|
});
|
|
}, [data]);
|
|
|
|
if (!data.length || !models.length) {
|
|
return (
|
|
<Card className="p-4">
|
|
<h3 className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-3">
|
|
{t("chartModelUsageOverTime")}
|
|
</h3>
|
|
<div className="text-center text-text-muted text-sm py-8">{t("chartNoData")}</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return <ModelOverTimeChartBody chartData={chartData} models={models} />;
|
|
}
|
|
|
|
function ModelOverTimeChartBody({ chartData, models }) {
|
|
const t = useTranslations("analytics");
|
|
const recharts = useRecharts();
|
|
|
|
if (!recharts) {
|
|
return <ChartLoadingCard className="p-4" />;
|
|
}
|
|
|
|
const { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } = recharts;
|
|
|
|
return (
|
|
<Card className="p-4">
|
|
<h3 className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-3">
|
|
{t("chartModelUsageOverTime")}
|
|
</h3>
|
|
<ResponsiveContainer width="100%" height={240}>
|
|
<AreaChart data={chartData} margin={{ top: 4, right: 4, left: 0, bottom: 0 }}>
|
|
<XAxis
|
|
dataKey="dateLabel"
|
|
tick={{ fontSize: 10, fill: "var(--color-text-muted)" }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
interval="preserveStartEnd"
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 10, fill: "var(--color-text-muted)" }}
|
|
tickFormatter={(v) => fmt(v)}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
width={50}
|
|
/>
|
|
<Tooltip content={<DarkTooltip formatter={fmt} />} />
|
|
{models.map((m, i) => (
|
|
<Area
|
|
key={m}
|
|
type="monotone"
|
|
dataKey={m}
|
|
stackId="1"
|
|
stroke={getModelColor(i)}
|
|
fill={getModelColor(i)}
|
|
fillOpacity={0.4}
|
|
strokeWidth={1.5}
|
|
animationDuration={600}
|
|
/>
|
|
))}
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-2 text-[10px] text-text-muted">
|
|
{models.map((m, i) => (
|
|
<span key={m} className="flex items-center gap-1">
|
|
<span
|
|
className="w-2 h-2 rounded-full shrink-0"
|
|
style={{ backgroundColor: getModelColor(i) }}
|
|
/>
|
|
{m}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// ── ProviderTable ──────────────────────────────────────────────────────────
|