mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +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
|
||||
*
|
||||
* 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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user