feat(ui): extend privacy mode to blur cost/token values in analytics

This commit is contained in:
kaitranntt
2025-12-17 16:40:05 -05:00
parent ee85a1fd82
commit 2bf7992a8a
9 changed files with 146 additions and 29 deletions
@@ -2,7 +2,7 @@
* Cache Efficiency Card Component * Cache Efficiency Card Component
* *
* Displays cache usage metrics including hit rate, savings estimate, * Displays cache usage metrics including hit rate, savings estimate,
* and cache read/write breakdown. * and cache read/write breakdown. Respects privacy mode to blur sensitive data.
*/ */
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -11,6 +11,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Database, TrendingUp, Zap } from 'lucide-react'; import { Database, TrendingUp, Zap } from 'lucide-react';
import type { UsageSummary } from '@/hooks/use-usage'; import type { UsageSummary } from '@/hooks/use-usage';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface CacheEfficiencyCardProps { interface CacheEfficiencyCardProps {
data: UsageSummary | undefined; data: UsageSummary | undefined;
@@ -19,6 +20,8 @@ interface CacheEfficiencyCardProps {
} }
export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficiencyCardProps) { export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficiencyCardProps) {
const { privacyMode } = usePrivacy();
const metrics = useMemo(() => { const metrics = useMemo(() => {
if (!data) return null; if (!data) return null;
@@ -93,7 +96,9 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
<div className="text-center"> <div className="text-center">
<div className="flex items-center justify-center gap-1.5 text-emerald-600 dark:text-emerald-400"> <div className="flex items-center justify-center gap-1.5 text-emerald-600 dark:text-emerald-400">
<TrendingUp className="w-5 h-5" /> <TrendingUp className="w-5 h-5" />
<span className="text-2xl font-bold">${metrics.estimatedSavings.toFixed(2)}</span> <span className={cn('text-2xl font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
${metrics.estimatedSavings.toFixed(2)}
</span>
</div> </div>
<p className="text-[11px] text-muted-foreground uppercase tracking-wider mt-0.5"> <p className="text-[11px] text-muted-foreground uppercase tracking-wider mt-0.5">
Estimated Savings Estimated Savings
@@ -106,21 +111,30 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
<div className="p-2 rounded-md bg-muted/50 border text-center"> <div className="p-2 rounded-md bg-muted/50 border text-center">
<div className="flex items-center justify-center gap-1"> <div className="flex items-center justify-center gap-1">
<Zap className="w-3.5 h-3.5 text-amber-500" /> <Zap className="w-3.5 h-3.5 text-amber-500" />
<span className="text-lg font-bold">{metrics.cacheHitRate.toFixed(0)}%</span> <span className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
{metrics.cacheHitRate.toFixed(0)}%
</span>
</div> </div>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Hit Rate</p> <p className="text-[10px] text-muted-foreground uppercase tracking-wider">Hit Rate</p>
</div> </div>
{/* Cache Cost */} {/* Cache Cost */}
<div className="p-2 rounded-md bg-muted/50 border text-center"> <div className="p-2 rounded-md bg-muted/50 border text-center">
<span className="text-lg font-bold">${metrics.cacheCost.toFixed(2)}</span> <span className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
${metrics.cacheCost.toFixed(2)}
</span>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Cache Cost</p> <p className="text-[10px] text-muted-foreground uppercase tracking-wider">Cache Cost</p>
</div> </div>
</div> </div>
{/* Cache breakdown bar */} {/* Cache breakdown bar */}
<div className="space-y-1"> <div className="space-y-1">
<div className="flex justify-between text-[10px] text-muted-foreground"> <div
className={cn(
'flex justify-between text-[10px] text-muted-foreground',
privacyMode && PRIVACY_BLUR_CLASS
)}
>
<span>Reads: {formatCompact(metrics.totalCacheReads)}</span> <span>Reads: {formatCompact(metrics.totalCacheReads)}</span>
<span>Writes: {formatCompact(metrics.totalCacheWrites)}</span> <span>Writes: {formatCompact(metrics.totalCacheWrites)}</span>
</div> </div>
@@ -2,7 +2,7 @@
* Model Breakdown Chart Component * Model Breakdown Chart Component
* *
* Displays usage distribution by model using pie chart. * Displays usage distribution by model using pie chart.
* Shows tokens, cost, and percentage breakdown. * Shows tokens, cost, and percentage breakdown. Respects privacy mode.
*/ */
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -10,6 +10,7 @@ import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import type { ModelUsage } from '@/hooks/use-usage'; import type { ModelUsage } from '@/hooks/use-usage';
import { cn, getModelColor } from '@/lib/utils'; import { cn, getModelColor } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface ModelBreakdownChartProps { interface ModelBreakdownChartProps {
data: ModelUsage[]; data: ModelUsage[];
@@ -18,6 +19,8 @@ interface ModelBreakdownChartProps {
} }
export function ModelBreakdownChart({ data, isLoading, className }: ModelBreakdownChartProps) { export function ModelBreakdownChart({ data, isLoading, className }: ModelBreakdownChartProps) {
const { privacyMode } = usePrivacy();
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!data || data.length === 0) return []; if (!data || data.length === 0) return [];
@@ -54,10 +57,12 @@ export function ModelBreakdownChart({ data, isLoading, className }: ModelBreakdo
return ( return (
<div className="rounded-lg border bg-background p-2 shadow-lg text-xs"> <div className="rounded-lg border bg-background p-2 shadow-lg text-xs">
<p className="font-medium mb-1">{item.name}</p> <p className="font-medium mb-1">{item.name}</p>
<p className="text-muted-foreground"> <p className={cn('text-muted-foreground', privacyMode && PRIVACY_BLUR_CLASS)}>
{formatNumber(item.value)} ({item.percentage.toFixed(1)}%) {formatNumber(item.value)} ({item.percentage.toFixed(1)}%)
</p> </p>
<p className="text-muted-foreground">${item.cost.toFixed(4)}</p> <p className={cn('text-muted-foreground', privacyMode && PRIVACY_BLUR_CLASS)}>
${item.cost.toFixed(4)}
</p>
</div> </div>
); );
}; };
@@ -1,12 +1,15 @@
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ArrowDownRight, ArrowUpRight, Database, Gauge, Sparkles } from 'lucide-react'; import { ArrowDownRight, ArrowUpRight, Database, Gauge, Sparkles } from 'lucide-react';
import type { ModelUsage } from '@/hooks/use-usage'; import type { ModelUsage } from '@/hooks/use-usage';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { cn } from '@/lib/utils';
interface ModelDetailsContentProps { interface ModelDetailsContentProps {
model: ModelUsage; model: ModelUsage;
} }
export function ModelDetailsContent({ model }: ModelDetailsContentProps) { export function ModelDetailsContent({ model }: ModelDetailsContentProps) {
const { privacyMode } = usePrivacy();
const ioRatioStatus = getIoRatioStatus(model.ioRatio); const ioRatioStatus = getIoRatioStatus(model.ioRatio);
return ( return (
@@ -32,11 +35,15 @@ export function ModelDetailsContent({ model }: ModelDetailsContentProps) {
{/* Stats Grid */} {/* Stats Grid */}
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="p-2 rounded-md bg-muted/50 border text-center"> <div className="p-2 rounded-md bg-muted/50 border text-center">
<p className="text-lg font-bold">${model.cost.toFixed(2)}</p> <p className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
${model.cost.toFixed(2)}
</p>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Total Cost</p> <p className="text-[10px] text-muted-foreground uppercase tracking-wider">Total Cost</p>
</div> </div>
<div className="p-2 rounded-md bg-muted/50 border text-center"> <div className="p-2 rounded-md bg-muted/50 border text-center">
<p className="text-lg font-bold">{formatCompactNumber(model.tokens)}</p> <p className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
{formatCompactNumber(model.tokens)}
</p>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Total Tokens</p> <p className="text-[10px] text-muted-foreground uppercase tracking-wider">Total Tokens</p>
</div> </div>
</div> </div>
@@ -46,7 +53,7 @@ export function ModelDetailsContent({ model }: ModelDetailsContentProps) {
<h5 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider"> <h5 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
Token Breakdown Token Breakdown
</h5> </h5>
<div className="space-y-1"> <div className={cn('space-y-1', privacyMode && PRIVACY_BLUR_CLASS)}>
<TokenRow <TokenRow
label="Input" label="Input"
tokens={model.inputTokens} tokens={model.inputTokens}
@@ -2,7 +2,7 @@
* Session Stats Card Component * Session Stats Card Component
* *
* Displays session usage metrics including active sessions, average duration, * Displays session usage metrics including active sessions, average duration,
* and session cost breakdown. * and session cost breakdown. Respects privacy mode to blur sensitive data.
*/ */
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -12,6 +12,7 @@ import { Clock, Users, Zap, Terminal } from 'lucide-react';
import type { PaginatedSessions } from '@/hooks/use-usage'; import type { PaginatedSessions } from '@/hooks/use-usage';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface SessionStatsCardProps { interface SessionStatsCardProps {
data: PaginatedSessions | undefined; data: PaginatedSessions | undefined;
@@ -20,6 +21,8 @@ interface SessionStatsCardProps {
} }
export function SessionStatsCard({ data, isLoading, className }: SessionStatsCardProps) { export function SessionStatsCard({ data, isLoading, className }: SessionStatsCardProps) {
const { privacyMode } = usePrivacy();
const stats = useMemo(() => { const stats = useMemo(() => {
if (!data?.sessions || data.sessions.length === 0) return null; if (!data?.sessions || data.sessions.length === 0) return null;
@@ -104,7 +107,9 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
<div className="p-2 rounded-md bg-muted/50 border text-center"> <div className="p-2 rounded-md bg-muted/50 border text-center">
<div className="flex items-center justify-center gap-1.5 text-green-600 dark:text-green-400"> <div className="flex items-center justify-center gap-1.5 text-green-600 dark:text-green-400">
<Zap className="w-4 h-4" /> <Zap className="w-4 h-4" />
<span className="text-xl font-bold">${stats.avgCost.toFixed(2)}</span> <span className={cn('text-xl font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
${stats.avgCost.toFixed(2)}
</span>
</div> </div>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mt-0.5"> <p className="text-[10px] text-muted-foreground uppercase tracking-wider mt-0.5">
Avg Cost/Session Avg Cost/Session
@@ -132,7 +137,7 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
{formatDistanceToNow(new Date(session.lastActivity), { addSuffix: true })} {formatDistanceToNow(new Date(session.lastActivity), { addSuffix: true })}
</span> </span>
</div> </div>
<div className="text-right shrink-0 ml-2"> <div className={cn('text-right shrink-0 ml-2', privacyMode && PRIVACY_BLUR_CLASS)}>
<div className="font-mono">${session.cost.toFixed(2)}</div> <div className="font-mono">${session.cost.toFixed(2)}</div>
<div className="text-[10px] text-muted-foreground"> <div className="text-[10px] text-muted-foreground">
{formatCompact(session.inputTokens + session.outputTokens)} toks {formatCompact(session.inputTokens + session.outputTokens)} toks
@@ -2,7 +2,7 @@
* Token Breakdown Chart Component * Token Breakdown Chart Component
* *
* Displays token usage breakdown by type (input, output, cache). * Displays token usage breakdown by type (input, output, cache).
* Shows stacked bar chart with cost breakdown. * Shows stacked bar chart with cost breakdown. Respects privacy mode.
*/ */
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -19,6 +19,7 @@ import {
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import type { TokenBreakdown } from '@/hooks/use-usage'; import type { TokenBreakdown } from '@/hooks/use-usage';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface TokenBreakdownChartProps { interface TokenBreakdownChartProps {
data?: TokenBreakdown; data?: TokenBreakdown;
@@ -34,6 +35,8 @@ const COLORS = {
}; };
export function TokenBreakdownChart({ data, isLoading, className }: TokenBreakdownChartProps) { export function TokenBreakdownChart({ data, isLoading, className }: TokenBreakdownChartProps) {
const { privacyMode } = usePrivacy();
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!data) return []; if (!data) return [];
@@ -151,7 +154,12 @@ export function TokenBreakdownChart({ data, isLoading, className }: TokenBreakdo
</ResponsiveContainer> </ResponsiveContainer>
{/* Cost breakdown summary */} {/* Cost breakdown summary */}
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-2 text-xs"> <div
className={cn(
'mt-4 grid grid-cols-2 md:grid-cols-4 gap-2 text-xs',
privacyMode && PRIVACY_BLUR_CLASS
)}
>
{chartData.map((item) => ( {chartData.map((item) => (
<div key={item.name} className="flex items-center gap-2 p-2 rounded-md bg-muted/50"> <div key={item.name} className="flex items-center gap-2 p-2 rounded-md bg-muted/50">
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: item.fill }} /> <div className="w-3 h-3 rounded-full" style={{ backgroundColor: item.fill }} />
@@ -3,6 +3,7 @@
* *
* Displays key metrics in a card grid layout. * Displays key metrics in a card grid layout.
* Shows total tokens, cost, cache tokens, and average cost per day. * Shows total tokens, cost, cache tokens, and average cost per day.
* Respects privacy mode to blur sensitive financial data.
*/ */
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
@@ -10,6 +11,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { DollarSign, Database, FileText, ArrowDownRight, ArrowUpRight } from 'lucide-react'; import { DollarSign, Database, FileText, ArrowDownRight, ArrowUpRight } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { UsageSummary } from '@/hooks/use-usage'; import type { UsageSummary } from '@/hooks/use-usage';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface UsageSummaryCardsProps { interface UsageSummaryCardsProps {
data?: UsageSummary; data?: UsageSummary;
@@ -17,6 +19,8 @@ interface UsageSummaryCardsProps {
} }
export function UsageSummaryCards({ data, isLoading }: UsageSummaryCardsProps) { export function UsageSummaryCards({ data, isLoading }: UsageSummaryCardsProps) {
const { privacyMode } = usePrivacy();
if (isLoading) { if (isLoading) {
return ( return (
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-4"> <div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-4">
@@ -100,9 +104,20 @@ export function UsageSummaryCards({ data, isLoading }: UsageSummaryCardsProps) {
<div className="flex items-center justify-between space-x-2"> <div className="flex items-center justify-between space-x-2">
<div className="space-y-1 min-w-0"> <div className="space-y-1 min-w-0">
<p className="text-xs font-medium text-muted-foreground truncate">{card.title}</p> <p className="text-xs font-medium text-muted-foreground truncate">{card.title}</p>
<p className="text-xl font-bold truncate">{card.format(card.value)}</p> <p
className={cn('text-xl font-bold truncate', privacyMode && PRIVACY_BLUR_CLASS)}
>
{card.format(card.value)}
</p>
{card.subtitle && ( {card.subtitle && (
<p className="text-[10px] text-muted-foreground truncate">{card.subtitle}</p> <p
className={cn(
'text-[10px] text-muted-foreground truncate',
privacyMode && PRIVACY_BLUR_CLASS
)}
>
{card.subtitle}
</p>
)} )}
</div> </div>
<div className={cn('p-2 rounded-lg shrink-0', card.bgColor)}> <div className={cn('p-2 rounded-lg shrink-0', card.bgColor)}>
@@ -3,6 +3,7 @@
* *
* Displays usage trends over time with tokens and cost. * Displays usage trends over time with tokens and cost.
* Supports daily, hourly, and monthly granularity with interactive tooltips. * Supports daily, hourly, and monthly granularity with interactive tooltips.
* Respects privacy mode to blur sensitive data.
*/ */
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -19,6 +20,7 @@ import { format } from 'date-fns';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { DailyUsage, HourlyUsage } from '@/hooks/use-usage'; import type { DailyUsage, HourlyUsage } from '@/hooks/use-usage';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
type ChartData = DailyUsage | HourlyUsage; type ChartData = DailyUsage | HourlyUsage;
@@ -35,6 +37,8 @@ export function UsageTrendChart({
granularity = 'daily', granularity = 'daily',
className, className,
}: UsageTrendChartProps) { }: UsageTrendChartProps) {
const { privacyMode } = usePrivacy();
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!data || data.length === 0) return []; if (!data || data.length === 0) return [];
@@ -66,6 +70,35 @@ export function UsageTrendChart({
); );
} }
// Custom tick component for privacy-aware axis labels
const PrivacyTick = ({
x,
y,
payload,
isRight,
}: {
x: number;
y: number;
payload: { value: string | number };
isRight?: boolean;
}) => {
const displayValue = isRight ? `$${payload.value}` : formatNumber(Number(payload.value));
return (
<text
x={x}
y={y}
dy={4}
textAnchor={isRight ? 'start' : 'end'}
fontSize={12}
fill="currentColor"
className={cn('fill-muted-foreground', privacyMode && 'blur-[4px]')}
>
{displayValue}
</text>
);
};
return ( return (
<div className={cn('w-full h-full', className)}> <div className={cn('w-full h-full', className)}>
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
@@ -93,19 +126,17 @@ export function UsageTrendChart({
<YAxis <YAxis
yAxisId="left" yAxisId="left"
orientation="left" orientation="left"
tick={{ fontSize: 12 }} tick={(props) => <PrivacyTick {...props} isRight={false} />}
tickLine={false} tickLine={false}
axisLine={{ className: 'stroke-muted' }} axisLine={{ className: 'stroke-muted' }}
tickFormatter={(value) => formatNumber(value)}
/> />
<YAxis <YAxis
yAxisId="right" yAxisId="right"
orientation="right" orientation="right"
tick={{ fontSize: 12 }} tick={(props) => <PrivacyTick {...props} isRight={true} />}
tickLine={false} tickLine={false}
axisLine={{ className: 'stroke-muted' }} axisLine={{ className: 'stroke-muted' }}
tickFormatter={(value) => `$${value}`}
/> />
<Tooltip <Tooltip
@@ -117,7 +148,11 @@ export function UsageTrendChart({
<div className="rounded-lg border bg-background p-3 shadow-lg"> <div className="rounded-lg border bg-background p-3 shadow-lg">
<p className="font-medium mb-2">{label}</p> <p className="font-medium mb-2">{label}</p>
{payload.map((entry, index) => ( {payload.map((entry, index) => (
<p key={index} className="text-sm" style={{ color: entry.color }}> <p
key={index}
className={cn('text-sm', privacyMode && PRIVACY_BLUR_CLASS)}
style={{ color: entry.color }}
>
{entry.name}:{' '} {entry.name}:{' '}
{entry.name === 'Tokens' {entry.name === 'Tokens'
? formatNumber(Number(entry.value) || 0) ? formatNumber(Number(entry.value) || 0)
@@ -125,7 +160,12 @@ export function UsageTrendChart({
</p> </p>
))} ))}
{'requests' in tooltipData && ( {'requests' in tooltipData && (
<p className="text-sm text-muted-foreground mt-1"> <p
className={cn(
'text-sm text-muted-foreground mt-1',
privacyMode && PRIVACY_BLUR_CLASS
)}
>
Requests: {tooltipData.requests} Requests: {tooltipData.requests}
</p> </p>
)} )}
+12 -2
View File
@@ -8,6 +8,7 @@
* - Token usage with cost estimation * - Token usage with cost estimation
* - Model breakdown with usage distribution * - Model breakdown with usage distribution
* - Session history * - Session history
* Respects privacy mode to blur sensitive data.
*/ */
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -26,12 +27,14 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats'; import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface CliproxyStatsOverviewProps { interface CliproxyStatsOverviewProps {
className?: string; className?: string;
} }
export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps) { export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps) {
const { privacyMode } = usePrivacy();
const { data: status, isLoading: statusLoading } = useCliproxyStatus(); const { data: status, isLoading: statusLoading } = useCliproxyStatus();
const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running); const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running);
@@ -216,8 +219,15 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Total Tokens</p> <p className="text-xs font-medium text-muted-foreground">Total Tokens</p>
<p className="text-2xl font-bold">{formatNumber(totalTokens)}</p> <p className={cn('text-2xl font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
<p className="text-[10px] text-muted-foreground"> {formatNumber(totalTokens)}
</p>
<p
className={cn(
'text-[10px] text-muted-foreground',
privacyMode && PRIVACY_BLUR_CLASS
)}
>
~${estimateCost(totalTokens).toFixed(2)} estimated ~${estimateCost(totalTokens).toFixed(2)} estimated
</p> </p>
</div> </div>
+16 -3
View File
@@ -30,7 +30,8 @@ import {
useSessions, useSessions,
type ModelUsage, type ModelUsage,
} from '@/hooks/use-usage'; } from '@/hooks/use-usage';
import { getModelColor } from '@/lib/utils'; import { getModelColor, cn } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
// Format token count to human-readable (K/M/B) // Format token count to human-readable (K/M/B)
function formatTokens(num: number): string { function formatTokens(num: number): string {
@@ -41,6 +42,8 @@ function formatTokens(num: number): string {
} }
export function AnalyticsPage() { export function AnalyticsPage() {
const { privacyMode } = usePrivacy();
// Default to last 30 days // Default to last 30 days
const [dateRange, setDateRange] = useState<DateRange | undefined>({ const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: subDays(new Date(), 30), from: subDays(new Date(), 30),
@@ -248,11 +251,21 @@ export function AnalyticsPage() {
</div> </div>
</div> </div>
{/* Token count */} {/* Token count */}
<span className="text-[10px] text-muted-foreground w-14 text-right shrink-0"> <span
className={cn(
'text-[10px] text-muted-foreground w-14 text-right shrink-0',
privacyMode && PRIVACY_BLUR_CLASS
)}
>
{formatTokens(model.tokens)} {formatTokens(model.tokens)}
</span> </span>
{/* Total cost */} {/* Total cost */}
<span className="font-mono font-medium w-16 text-right shrink-0"> <span
className={cn(
'font-mono font-medium w-16 text-right shrink-0',
privacyMode && PRIVACY_BLUR_CLASS
)}
>
${model.cost.toFixed(2)} ${model.cost.toFixed(2)}
</span> </span>
<ChevronRight className="w-3 h-3 opacity-0 group-hover:opacity-50 transition-opacity shrink-0" /> <ChevronRight className="w-3 h-3 opacity-0 group-hover:opacity-50 transition-opacity shrink-0" />