diff --git a/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx b/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx new file mode 100644 index 0000000000..4798fbba2e --- /dev/null +++ b/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Card } from "@/shared/components"; +import { useTranslations } from "next-intl"; + +interface CacheMetrics { + totalRequests: number; + requestsWithCacheControl: number; + totalInputTokens: number; + totalCachedTokens: number; + totalCacheCreationTokens: number; + tokensSaved: number; + estimatedCostSaved: number; + byProvider: Record< + string, + { + requests: number; + inputTokens: number; + cachedTokens: number; + cacheCreationTokens: number; + } + >; + byStrategy: Record< + string, + { + requests: number; + inputTokens: number; + cachedTokens: number; + cacheCreationTokens: number; + } + >; + lastUpdated: string; +} + +const REFRESH_INTERVAL_MS = 10_000; +const REFRESH_INTERVAL_SECONDS = REFRESH_INTERVAL_MS / 1000; + +export default function CacheStatsCard() { + const [metrics, setMetrics] = useState(null); + const [resetting, setResetting] = useState(false); + const t = useTranslations("cache"); + + const fetchMetrics = useCallback(() => { + fetch("/api/settings/cache-metrics") + .then((r) => r.json()) + .then(setMetrics) + .catch(() => {}); + }, []); + + useEffect(() => { + void fetchMetrics(); + const id = setInterval(() => void fetchMetrics(), REFRESH_INTERVAL_MS); + return () => clearInterval(id); + }, [fetchMetrics]); + + const handleReset = async () => { + setResetting(true); + try { + await fetch("/api/settings/cache-metrics", { method: "DELETE" }); + fetchMetrics(); + } finally { + setResetting(false); + } + }; + + const cacheHitRate = + metrics && metrics.totalInputTokens > 0 + ? (metrics.totalCachedTokens / metrics.totalInputTokens) * 100 + : 0; + + return ( + +
+
+
+ +

{t("cacheMetrics")}

+
+
+ + {t("autoRefresh", { seconds: REFRESH_INTERVAL_SECONDS })} + + +
+
+ + {metrics ? ( +
+ {/* Overview Stats */} +
+
+

{t("totalRequests")}

+

{metrics.totalRequests}

+
+
+

{t("withCacheControl")}

+

+ {metrics.requestsWithCacheControl} +

+
+
+ + {/* Token Stats */} +
+
+

{t("inputTokens")}

+

+ {metrics.totalInputTokens.toLocaleString()} +

+
+
+

{t("cachedTokensRead")}

+

+ {metrics.totalCachedTokens.toLocaleString()} +

+
+
+

{t("cacheCreationWrite")}

+

+ {metrics.totalCacheCreationTokens.toLocaleString()} +

+
+
+ + {/* Cache Ratio */} +
+
+
+

{t("cacheReuseRatio")}

+

{t("cacheReuseRatioDesc")}

+
+

{cacheHitRate.toFixed(1)}%

+
+ {/* Progress bar */} +
+
+
+
+ + {/* Savings */} +
+
+

{t("tokensSaved")}

+

+ {metrics.tokensSaved.toLocaleString()} +

+
+
+

{t("estCostSaved")}

+

+ ${metrics.estimatedCostSaved.toFixed(4)} +

+
+
+ + {/* By Provider */} + {Object.keys(metrics.byProvider).length > 0 && ( +
+

{t("byProvider")}

+
+ {Object.entries(metrics.byProvider).map(([provider, stats]) => { + const providerCacheRate = + stats.inputTokens > 0 ? (stats.cachedTokens / stats.inputTokens) * 100 : 0; + return ( +
+
+ {provider} + + {stats.requests} {t("requestsShort")} + +
+
+ + {t("inputShort")}: {stats.inputTokens.toLocaleString()} + + + {t("cachedShort")}: {stats.cachedTokens.toLocaleString()} + + + {t("writeShort")}: {stats.cacheCreationTokens.toLocaleString()} + + + {providerCacheRate.toFixed(0)}% + +
+
+ ); + })} +
+
+ )} +
+ ) : ( +

{t("loading")}

+ )} +
+ + ); +}