mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-05 14:22:52 +00:00
feat(ui): extend privacy mode to blur cost/token values in analytics
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user