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
*
* 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';
@@ -11,6 +11,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Database, TrendingUp, Zap } from 'lucide-react';
import type { UsageSummary } from '@/hooks/use-usage';
import { cn } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface CacheEfficiencyCardProps {
data: UsageSummary | undefined;
@@ -19,6 +20,8 @@ interface CacheEfficiencyCardProps {
}
export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficiencyCardProps) {
const { privacyMode } = usePrivacy();
const metrics = useMemo(() => {
if (!data) return null;
@@ -93,7 +96,9 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
<div className="text-center">
<div className="flex items-center justify-center gap-1.5 text-emerald-600 dark:text-emerald-400">
<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>
<p className="text-[11px] text-muted-foreground uppercase tracking-wider mt-0.5">
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="flex items-center justify-center gap-1">
<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>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Hit Rate</p>
</div>
{/* Cache Cost */}
<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>
</div>
</div>
{/* Cache breakdown bar */}
<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>Writes: {formatCompact(metrics.totalCacheWrites)}</span>
</div>
@@ -2,7 +2,7 @@
* Model Breakdown Chart Component
*
* 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';
@@ -10,6 +10,7 @@ import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
import { Skeleton } from '@/components/ui/skeleton';
import type { ModelUsage } from '@/hooks/use-usage';
import { cn, getModelColor } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface ModelBreakdownChartProps {
data: ModelUsage[];
@@ -18,6 +19,8 @@ interface ModelBreakdownChartProps {
}
export function ModelBreakdownChart({ data, isLoading, className }: ModelBreakdownChartProps) {
const { privacyMode } = usePrivacy();
const chartData = useMemo(() => {
if (!data || data.length === 0) return [];
@@ -54,10 +57,12 @@ export function ModelBreakdownChart({ data, isLoading, className }: ModelBreakdo
return (
<div className="rounded-lg border bg-background p-2 shadow-lg text-xs">
<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)}%)
</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>
);
};
@@ -1,12 +1,15 @@
import { Badge } from '@/components/ui/badge';
import { ArrowDownRight, ArrowUpRight, Database, Gauge, Sparkles } from 'lucide-react';
import type { ModelUsage } from '@/hooks/use-usage';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { cn } from '@/lib/utils';
interface ModelDetailsContentProps {
model: ModelUsage;
}
export function ModelDetailsContent({ model }: ModelDetailsContentProps) {
const { privacyMode } = usePrivacy();
const ioRatioStatus = getIoRatioStatus(model.ioRatio);
return (
@@ -32,11 +35,15 @@ export function ModelDetailsContent({ model }: ModelDetailsContentProps) {
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-2">
<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>
</div>
<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>
</div>
</div>
@@ -46,7 +53,7 @@ export function ModelDetailsContent({ model }: ModelDetailsContentProps) {
<h5 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
Token Breakdown
</h5>
<div className="space-y-1">
<div className={cn('space-y-1', privacyMode && PRIVACY_BLUR_CLASS)}>
<TokenRow
label="Input"
tokens={model.inputTokens}
@@ -2,7 +2,7 @@
* Session Stats Card Component
*
* 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';
@@ -12,6 +12,7 @@ import { Clock, Users, Zap, Terminal } from 'lucide-react';
import type { PaginatedSessions } from '@/hooks/use-usage';
import { cn } from '@/lib/utils';
import { formatDistanceToNow } from 'date-fns';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface SessionStatsCardProps {
data: PaginatedSessions | undefined;
@@ -20,6 +21,8 @@ interface SessionStatsCardProps {
}
export function SessionStatsCard({ data, isLoading, className }: SessionStatsCardProps) {
const { privacyMode } = usePrivacy();
const stats = useMemo(() => {
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="flex items-center justify-center gap-1.5 text-green-600 dark:text-green-400">
<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>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mt-0.5">
Avg Cost/Session
@@ -132,7 +137,7 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
{formatDistanceToNow(new Date(session.lastActivity), { addSuffix: true })}
</span>
</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="text-[10px] text-muted-foreground">
{formatCompact(session.inputTokens + session.outputTokens)} toks
@@ -2,7 +2,7 @@
* Token Breakdown Chart Component
*
* 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';
@@ -19,6 +19,7 @@ import {
import { Skeleton } from '@/components/ui/skeleton';
import type { TokenBreakdown } from '@/hooks/use-usage';
import { cn } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface TokenBreakdownChartProps {
data?: TokenBreakdown;
@@ -34,6 +35,8 @@ const COLORS = {
};
export function TokenBreakdownChart({ data, isLoading, className }: TokenBreakdownChartProps) {
const { privacyMode } = usePrivacy();
const chartData = useMemo(() => {
if (!data) return [];
@@ -151,7 +154,12 @@ export function TokenBreakdownChart({ data, isLoading, className }: TokenBreakdo
</ResponsiveContainer>
{/* 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) => (
<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 }} />
@@ -3,6 +3,7 @@
*
* Displays key metrics in a card grid layout.
* 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';
@@ -10,6 +11,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { DollarSign, Database, FileText, ArrowDownRight, ArrowUpRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { UsageSummary } from '@/hooks/use-usage';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface UsageSummaryCardsProps {
data?: UsageSummary;
@@ -17,6 +19,8 @@ interface UsageSummaryCardsProps {
}
export function UsageSummaryCards({ data, isLoading }: UsageSummaryCardsProps) {
const { privacyMode } = usePrivacy();
if (isLoading) {
return (
<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="space-y-1 min-w-0">
<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 && (
<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 className={cn('p-2 rounded-lg shrink-0', card.bgColor)}>
@@ -3,6 +3,7 @@
*
* Displays usage trends over time with tokens and cost.
* Supports daily, hourly, and monthly granularity with interactive tooltips.
* Respects privacy mode to blur sensitive data.
*/
import { useMemo } from 'react';
@@ -19,6 +20,7 @@ import { format } from 'date-fns';
import { Skeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils';
import type { DailyUsage, HourlyUsage } from '@/hooks/use-usage';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
type ChartData = DailyUsage | HourlyUsage;
@@ -35,6 +37,8 @@ export function UsageTrendChart({
granularity = 'daily',
className,
}: UsageTrendChartProps) {
const { privacyMode } = usePrivacy();
const chartData = useMemo(() => {
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 (
<div className={cn('w-full h-full', className)}>
<ResponsiveContainer width="100%" height="100%">
@@ -93,19 +126,17 @@ export function UsageTrendChart({
<YAxis
yAxisId="left"
orientation="left"
tick={{ fontSize: 12 }}
tick={(props) => <PrivacyTick {...props} isRight={false} />}
tickLine={false}
axisLine={{ className: 'stroke-muted' }}
tickFormatter={(value) => formatNumber(value)}
/>
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12 }}
tick={(props) => <PrivacyTick {...props} isRight={true} />}
tickLine={false}
axisLine={{ className: 'stroke-muted' }}
tickFormatter={(value) => `$${value}`}
/>
<Tooltip
@@ -117,7 +148,11 @@ export function UsageTrendChart({
<div className="rounded-lg border bg-background p-3 shadow-lg">
<p className="font-medium mb-2">{label}</p>
{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 === 'Tokens'
? formatNumber(Number(entry.value) || 0)
@@ -125,7 +160,12 @@ export function UsageTrendChart({
</p>
))}
{'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}
</p>
)}
+12 -2
View File
@@ -8,6 +8,7 @@
* - Token usage with cost estimation
* - Model breakdown with usage distribution
* - Session history
* Respects privacy mode to blur sensitive data.
*/
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -26,12 +27,14 @@ import {
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface CliproxyStatsOverviewProps {
className?: string;
}
export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps) {
const { privacyMode } = usePrivacy();
const { data: status, isLoading: statusLoading } = useCliproxyStatus();
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="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Total Tokens</p>
<p className="text-2xl font-bold">{formatNumber(totalTokens)}</p>
<p className="text-[10px] text-muted-foreground">
<p className={cn('text-2xl font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
{formatNumber(totalTokens)}
</p>
<p
className={cn(
'text-[10px] text-muted-foreground',
privacyMode && PRIVACY_BLUR_CLASS
)}
>
~${estimateCost(totalTokens).toFixed(2)} estimated
</p>
</div>
+16 -3
View File
@@ -30,7 +30,8 @@ import {
useSessions,
type ModelUsage,
} 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)
function formatTokens(num: number): string {
@@ -41,6 +42,8 @@ function formatTokens(num: number): string {
}
export function AnalyticsPage() {
const { privacyMode } = usePrivacy();
// Default to last 30 days
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: subDays(new Date(), 30),
@@ -248,11 +251,21 @@ export function AnalyticsPage() {
</div>
</div>
{/* 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)}
</span>
{/* 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)}
</span>
<ChevronRight className="w-3 h-3 opacity-0 group-hover:opacity-50 transition-opacity shrink-0" />