|
|
@@ -1,11 +1,24 @@
|
|
|
"use client";
|
|
|
|
|
|
-import { BarChart3, RefreshCw } from "lucide-react";
|
|
|
+import { format } from "date-fns";
|
|
|
+import {
|
|
|
+ Activity,
|
|
|
+ ArrowDownRight,
|
|
|
+ ArrowUpRight,
|
|
|
+ BarChart3,
|
|
|
+ Coins,
|
|
|
+ Database,
|
|
|
+ Hash,
|
|
|
+ Percent,
|
|
|
+ RefreshCw,
|
|
|
+ Target,
|
|
|
+} from "lucide-react";
|
|
|
import { useTranslations } from "next-intl";
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
import { getMyStatsSummary, type MyStatsSummary } from "@/actions/my-usage";
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
import { formatTokenAmount } from "@/lib/utils";
|
|
|
@@ -26,7 +39,7 @@ export function StatisticsSummaryCard({
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
const [dateRange, setDateRange] = useState<{ startDate?: string; endDate?: string }>(() => {
|
|
|
- const today = new Date().toISOString().split("T")[0];
|
|
|
+ const today = format(new Date(), "yyyy-MM-dd");
|
|
|
return { startDate: today, endDate: today };
|
|
|
});
|
|
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
@@ -219,7 +232,10 @@ export function StatisticsSummaryCard({
|
|
|
cost={item.cost}
|
|
|
inputTokens={item.inputTokens}
|
|
|
outputTokens={item.outputTokens}
|
|
|
+ cacheCreationTokens={item.cacheCreationTokens}
|
|
|
+ cacheReadTokens={item.cacheReadTokens}
|
|
|
currencyCode={currencyCode}
|
|
|
+ totalCost={stats.totalCost}
|
|
|
/>
|
|
|
))}
|
|
|
</div>
|
|
|
@@ -243,7 +259,10 @@ export function StatisticsSummaryCard({
|
|
|
cost={item.cost}
|
|
|
inputTokens={item.inputTokens}
|
|
|
outputTokens={item.outputTokens}
|
|
|
+ cacheCreationTokens={item.cacheCreationTokens}
|
|
|
+ cacheReadTokens={item.cacheReadTokens}
|
|
|
currencyCode={currencyCode}
|
|
|
+ totalCost={stats.totalCost}
|
|
|
/>
|
|
|
))}
|
|
|
</div>
|
|
|
@@ -268,7 +287,10 @@ interface ModelBreakdownRowProps {
|
|
|
cost: number;
|
|
|
inputTokens: number;
|
|
|
outputTokens: number;
|
|
|
+ cacheCreationTokens: number;
|
|
|
+ cacheReadTokens: number;
|
|
|
currencyCode: CurrencyCode;
|
|
|
+ totalCost: number;
|
|
|
}
|
|
|
|
|
|
function ModelBreakdownRow({
|
|
|
@@ -277,21 +299,196 @@ function ModelBreakdownRow({
|
|
|
cost,
|
|
|
inputTokens,
|
|
|
outputTokens,
|
|
|
+ cacheCreationTokens,
|
|
|
+ cacheReadTokens,
|
|
|
currencyCode,
|
|
|
+ totalCost,
|
|
|
}: ModelBreakdownRowProps) {
|
|
|
+ const [open, setOpen] = useState(false);
|
|
|
const t = useTranslations("myUsage.stats");
|
|
|
|
|
|
+ const totalAllTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
|
|
|
+ const totalInputTokens = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
|
+ const cacheHitRate =
|
|
|
+ totalInputTokens > 0 ? ((cacheReadTokens / totalInputTokens) * 100).toFixed(1) : "0.0";
|
|
|
+ const costPercentage = totalCost > 0 ? ((cost / totalCost) * 100).toFixed(1) : "0.0";
|
|
|
+
|
|
|
+ const cacheHitRateNum = Number.parseFloat(cacheHitRate);
|
|
|
+ const cacheHitColor =
|
|
|
+ cacheHitRateNum >= 85
|
|
|
+ ? "text-green-600 dark:text-green-400"
|
|
|
+ : cacheHitRateNum >= 60
|
|
|
+ ? "text-yellow-600 dark:text-yellow-400"
|
|
|
+ : "text-orange-600 dark:text-orange-400";
|
|
|
+
|
|
|
return (
|
|
|
- <div className="flex items-center justify-between rounded-md border px-3 py-2">
|
|
|
- <div className="flex flex-col text-sm min-w-0">
|
|
|
- <span className="font-medium text-foreground truncate">{model || t("unknownModel")}</span>
|
|
|
- <span className="text-xs text-muted-foreground">
|
|
|
- {requests.toLocaleString()} req · {formatTokenAmount(inputTokens + outputTokens)} tok
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div className="text-right text-sm font-semibold text-foreground whitespace-nowrap ml-2">
|
|
|
- {formatCurrency(cost, currencyCode)}
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ role="button"
|
|
|
+ tabIndex={0}
|
|
|
+ className="flex items-center justify-between rounded-md border px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors group"
|
|
|
+ onClick={() => setOpen(true)}
|
|
|
+ onKeyDown={(e) => {
|
|
|
+ if (e.key === "Enter" || e.key === " ") {
|
|
|
+ e.preventDefault();
|
|
|
+ setOpen(true);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div className="flex flex-col text-sm min-w-0 gap-1">
|
|
|
+ <span className="font-medium text-foreground truncate">{model || t("unknownModel")}</span>
|
|
|
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
|
+ <span className="flex items-center gap-1">
|
|
|
+ <Activity className="h-3 w-3" />
|
|
|
+ {requests.toLocaleString()}
|
|
|
+ </span>
|
|
|
+ <span className="flex items-center gap-1">
|
|
|
+ <Hash className="h-3 w-3" />
|
|
|
+ {formatTokenAmount(totalAllTokens)}
|
|
|
+ </span>
|
|
|
+ <span className={`flex items-center gap-1 ${cacheHitColor}`}>
|
|
|
+ <Target className="h-3 w-3" />
|
|
|
+ {cacheHitRate}%
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className="text-right text-sm font-semibold text-foreground whitespace-nowrap ml-2">
|
|
|
+ <div>{formatCurrency(cost, currencyCode)}</div>
|
|
|
+ <div className="text-xs text-muted-foreground font-normal">({costPercentage}%)</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+
|
|
|
+ <Dialog open={open} onOpenChange={setOpen}>
|
|
|
+ <DialogContent className="sm:max-w-lg">
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle className="flex items-center gap-2 text-lg">
|
|
|
+ <Database className="h-5 w-5 text-primary" />
|
|
|
+ {model || t("unknownModel")}
|
|
|
+ </DialogTitle>
|
|
|
+ </DialogHeader>
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="grid grid-cols-3 gap-3">
|
|
|
+ <div className="rounded-lg border bg-muted/50 p-3 space-y-1">
|
|
|
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
+ <Activity className="h-3.5 w-3.5" />
|
|
|
+ {t("modal.requests")}
|
|
|
+ </div>
|
|
|
+ <div className="text-lg font-semibold font-mono">{requests.toLocaleString()}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="rounded-lg border bg-muted/50 p-3 space-y-1">
|
|
|
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
+ <Hash className="h-3.5 w-3.5" />
|
|
|
+ {t("modal.totalTokens")}
|
|
|
+ </div>
|
|
|
+ <div className="text-lg font-semibold font-mono">
|
|
|
+ {formatTokenAmount(totalAllTokens)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="rounded-lg border bg-muted/50 p-3 space-y-1">
|
|
|
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
+ <Coins className="h-3.5 w-3.5" />
|
|
|
+ {t("modal.cost")}
|
|
|
+ </div>
|
|
|
+ <div className="text-lg font-semibold font-mono">
|
|
|
+ {formatCurrency(cost, currencyCode)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Separator />
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <h4 className="text-sm font-medium flex items-center gap-1.5">
|
|
|
+ <Hash className="h-4 w-4 text-muted-foreground" />
|
|
|
+ {t("modal.totalTokens")}
|
|
|
+ </h4>
|
|
|
+ <div className="grid grid-cols-2 gap-3">
|
|
|
+ <div className="rounded-lg border p-3 space-y-1">
|
|
|
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
+ <ArrowUpRight className="h-3.5 w-3.5 text-blue-500" />
|
|
|
+ {t("modal.inputTokens")}
|
|
|
+ </div>
|
|
|
+ <div className="text-base font-semibold font-mono">
|
|
|
+ {formatTokenAmount(inputTokens)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="rounded-lg border p-3 space-y-1">
|
|
|
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
+ <ArrowDownRight className="h-3.5 w-3.5 text-purple-500" />
|
|
|
+ {t("modal.outputTokens")}
|
|
|
+ </div>
|
|
|
+ <div className="text-base font-semibold font-mono">
|
|
|
+ {formatTokenAmount(outputTokens)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Separator />
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <h4 className="text-sm font-medium flex items-center gap-1.5">
|
|
|
+ <Database className="h-4 w-4 text-muted-foreground" />
|
|
|
+ {t("modal.cacheTokens")}
|
|
|
+ </h4>
|
|
|
+ <div className="grid grid-cols-2 gap-3">
|
|
|
+ <div className="rounded-lg border p-3 space-y-1">
|
|
|
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
+ <Database className="h-3.5 w-3.5 text-orange-500" />
|
|
|
+ {t("modal.cacheWrite")}
|
|
|
+ </div>
|
|
|
+ <div className="text-base font-semibold font-mono">
|
|
|
+ {formatTokenAmount(cacheCreationTokens)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="rounded-lg border p-3 space-y-1">
|
|
|
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
+ <Database className="h-3.5 w-3.5 text-green-500" />
|
|
|
+ {t("modal.cacheRead")}
|
|
|
+ </div>
|
|
|
+ <div className="text-base font-semibold font-mono">
|
|
|
+ {formatTokenAmount(cacheReadTokens)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="rounded-lg border bg-gradient-to-r from-muted/50 to-muted/30 p-3 mt-2">
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div className="flex items-center gap-1.5 text-sm font-medium">
|
|
|
+ <Target className="h-4 w-4" />
|
|
|
+ {t("modal.cacheHitRate")}
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className={`text-lg font-bold font-mono ${cacheHitColor}`}>
|
|
|
+ {cacheHitRate}%
|
|
|
+ </span>
|
|
|
+ <span
|
|
|
+ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
|
+ cacheHitRateNum >= 85
|
|
|
+ ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
|
|
+ : cacheHitRateNum >= 60
|
|
|
+ ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
|
|
|
+ : "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <Percent className="h-3 w-3" />
|
|
|
+ {cacheHitRateNum >= 85
|
|
|
+ ? t("modal.performanceHigh")
|
|
|
+ : cacheHitRateNum >= 60
|
|
|
+ ? t("modal.performanceMedium")
|
|
|
+ : t("modal.performanceLow")}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+ </>
|
|
|
);
|
|
|
}
|