Merge pull request #122 from kaitranntt/dev

feat(release): v6.1.0 - UI enhancements and account flow improvements
This commit is contained in:
Kai (Tam Nhu) Tran
2025-12-17 19:01:15 -05:00
committed by GitHub
22 changed files with 1130 additions and 491 deletions
+1 -1
View File
@@ -1 +1 @@
6.1.0
6.1.0-dev.6
+1 -1
View File
@@ -5,6 +5,6 @@
"ANTHROPIC_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gemini-3-pro-preview",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemini-2.5-flash"
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemini-3-flash-preview"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kaitranntt/ccs",
"version": "6.1.0",
"version": "6.1.0-dev.6",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"keywords": [
"cli",
+1 -35
View File
@@ -13,20 +13,6 @@ import { getProviderSettingsPath, getClaudeEnvVars } from './config-generator';
import { CLIProxyProvider } from './types';
import { initUI, color, bold, dim, ok, info, header } from '../utils/ui';
/**
* Check if model is a Claude model routed via Antigravity
* Claude models require MAX_THINKING_TOKENS < 8192 for thinking to work
*/
function isClaudeModel(modelId: string): boolean {
return modelId.includes('claude');
}
/**
* Max thinking tokens for Claude models via Antigravity
* Must be < 8192 due to Google protocol conversion limitations
*/
const CLAUDE_MAX_THINKING_TOKENS = '8191';
/** CCS directory */
const CCS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.ccs');
@@ -163,8 +149,7 @@ export async function configureProviderModel(
// Build settings with selective merge:
// - Preserve ALL user settings (top-level and env vars)
// - Only update CCS-controlled fields (model selection + thinking toggle for Claude)
const isClaude = isClaudeModel(selectedModel);
// - Only update CCS-controlled fields (model selection)
// CCS-controlled env vars (always override with our values)
const ccsControlledEnv: Record<string, string> = {
@@ -176,22 +161,12 @@ export async function configureProviderModel(
ANTHROPIC_DEFAULT_HAIKU_MODEL: baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL || '',
};
// Claude models require MAX_THINKING_TOKENS < 8192 for thinking to work
if (isClaude) {
ccsControlledEnv.MAX_THINKING_TOKENS = CLAUDE_MAX_THINKING_TOKENS;
}
// Merge: user env vars (preserved) + CCS controlled (override)
const mergedEnv = {
...existingEnv,
...ccsControlledEnv,
};
// Remove MAX_THINKING_TOKENS when switching away from Claude model
if (!isClaude && mergedEnv.MAX_THINKING_TOKENS) {
delete mergedEnv.MAX_THINKING_TOKENS;
}
// Build final settings: preserve user top-level settings + update env
const settings: Record<string, unknown> = {
...existingSettings,
@@ -222,15 +197,6 @@ export async function configureProviderModel(
console.error(dim(` ${reason}`));
console.error(dim(' Consider using a non-deprecated model for better compatibility.'));
}
// Show info for Claude models about thinking token limit
if (isClaude) {
console.error('');
console.error(
info(`MAX_THINKING_TOKENS set to ${CLAUDE_MAX_THINKING_TOKENS} (required < 8192)`)
);
console.error(dim(' Google protocol conversion requires this limit for thinking to work.'));
}
console.error('');
return true;
+2 -2
View File
@@ -115,7 +115,7 @@ describe('Model Config', () => {
ANTHROPIC_MODEL: 'gemini-claude-opus-4-5-thinking',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-claude-opus-4-5-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-claude-opus-4-5-thinking',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-2.5-flash',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-3-flash-preview',
},
};
@@ -138,7 +138,7 @@ describe('Model Config', () => {
ANTHROPIC_MODEL: 'gemini-claude-opus-4-5-thinking',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-claude-opus-4-5-thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-claude-opus-4-5-thinking',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-2.5-flash',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-3-flash-preview',
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because it is too large Load Diff
@@ -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,16 +2,18 @@
* Date Range Filter Component
*
* Provides date range selection with preset options for analytics.
* Uses react-day-picker for date selection UI.
* Uses react-day-picker for date selection UI within a Popover.
*/
import React from 'react';
import { format } from 'date-fns';
import type { DateRange } from 'react-day-picker';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { format, subDays } from 'date-fns';
import { CalendarIcon } from 'lucide-react';
import type { DateRange } from 'react-day-picker';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
interface DateRangeFilterProps {
value?: DateRange;
@@ -26,70 +28,94 @@ interface DateRangeFilterProps {
export function DateRangeFilter({
value,
onChange,
presets = [],
presets = [
{
label: 'Last 7 days',
range: {
from: subDays(new Date(), 7),
to: new Date(),
},
},
{
label: 'Last 30 days',
range: {
from: subDays(new Date(), 30),
to: new Date(),
},
},
{
label: 'Last 90 days',
range: {
from: subDays(new Date(), 90),
to: new Date(),
},
},
],
className,
}: DateRangeFilterProps) {
const handlePresetClick = (range: DateRange) => {
onChange(range);
};
const [isOpen, setIsOpen] = React.useState(false);
const handleFromChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const from = e.target.value ? new Date(e.target.value) : undefined;
onChange({ from, to: value?.to });
};
// Helper to check if a preset is currently selected
const isPresetSelected = (presetRange: DateRange) => {
if (!value || !value.from || !value.to || !presetRange.from || !presetRange.to) {
return false;
}
const handleToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const to = e.target.value ? new Date(e.target.value) : undefined;
onChange({ from: value?.from, to });
// Compare dates (ignoring time components if needed, but day-picker usually returns start of day)
// Simple comparison using formatted strings or timestamps
return (
format(value.from, 'yyyy-MM-dd') === format(presetRange.from, 'yyyy-MM-dd') &&
format(value.to, 'yyyy-MM-dd') === format(presetRange.to, 'yyyy-MM-dd')
);
};
return (
<div className={cn('flex flex-wrap items-center gap-2', className)}>
{/* Preset Buttons */}
{presets.map((preset, index) => (
<div className={cn('flex items-center gap-2', className)}>
{presets.map((preset) => (
<Button
key={index}
variant={isSameRange(value, preset.range) ? 'default' : 'outline'}
key={preset.label}
variant={isPresetSelected(preset.range) ? 'default' : 'outline'}
size="sm"
onClick={() => handlePresetClick(preset.range)}
onClick={() => onChange(preset.range)}
>
{preset.label}
</Button>
))}
{/* Custom Date Range Inputs */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
<Input
type="date"
value={value?.from ? format(value.from, 'yyyy-MM-dd') : ''}
onChange={handleFromChange}
placeholder="From"
className="w-40"
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
id="date"
variant={'outline'}
className={cn(
'w-auto min-w-[240px] justify-start text-left font-normal',
!value && 'text-muted-foreground'
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value?.from ? (
value.to ? (
<>
{format(value.from, 'LLL dd, y')} - {format(value.to, 'LLL dd, y')}
</>
) : (
format(value.from, 'LLL dd, y')
)
) : (
<span>Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<Calendar
initialFocus
mode="range"
defaultMonth={value?.from}
selected={value}
onSelect={onChange}
numberOfMonths={2}
/>
</div>
<span className="text-muted-foreground">to</span>
<Input
type="date"
value={value?.to ? format(value.to, 'yyyy-MM-dd') : ''}
onChange={handleToChange}
placeholder="To"
className="w-40"
/>
</div>
</PopoverContent>
</Popover>
</div>
);
}
// Helper to compare date ranges
function isSameRange(a?: DateRange, b?: DateRange): boolean {
if (!a || !b) return a === b;
const fromA = a.from?.getTime() ?? 0;
const fromB = b.from?.getTime() ?? 0;
const toA = a.to?.getTime() ?? 0;
const toB = b.to?.getTime() ?? 0;
return fromA === fromB && toA === toB;
}
@@ -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>
)}
+49
View File
@@ -0,0 +1,49 @@
/**
* ClaudeKit Badge Button
*
* "Powered by ClaudeKit" badge for navbar, inspired by landing page design.
* Compact version optimized for header placement.
*/
import { cn } from '@/lib/utils';
const CLAUDEKIT_URL = 'https://claudekit.cc?ref=HMNKXOHN';
export function ClaudeKitBadge() {
return (
<a
href={CLAUDEKIT_URL}
target="_blank"
rel="noopener noreferrer"
className={cn(
'group inline-flex items-center gap-2 px-3 py-1.5 rounded-lg',
'bg-accent/10 border-2 border-accent/40',
'hover:bg-accent hover:border-accent',
'transition-all duration-200 shadow-sm hover:shadow-md'
)}
title="Powered by ClaudeKit Framework"
>
<img src="/logos/claudekit-logo.png" alt="ClaudeKit" className="w-5 h-5" />
<span className="flex items-baseline gap-1.5 whitespace-nowrap">
<span
className={cn(
'text-[10px] font-medium uppercase tracking-wide',
'text-muted-foreground group-hover:text-accent-foreground/80',
'transition-colors'
)}
>
Powered by
</span>
<span
className={cn(
'text-xs font-bold text-foreground',
'group-hover:text-accent-foreground',
'transition-colors'
)}
>
ClaudeKit
</span>
</span>
</a>
);
}
+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>
+6 -1
View File
@@ -9,6 +9,8 @@ import { DocsLink } from '@/components/docs-link';
import { ConnectionIndicator } from '@/components/connection-indicator';
import { LocalhostDisclaimer } from '@/components/localhost-disclaimer';
import { Skeleton } from '@/components/ui/skeleton';
import { ClaudeKitBadge } from '@/components/claudekit-badge';
import { SponsorButton } from '@/components/sponsor-button';
function PageLoader() {
return (
@@ -25,7 +27,10 @@ export function Layout() {
<AppSidebar />
<main className="flex-1 flex flex-col min-h-0 overflow-hidden bg-background">
<header className="flex h-14 items-center justify-between px-6 border-b shrink-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="font-semibold text-lg tracking-tight">CCS Config</div>
<div className="flex items-center gap-3">
<ClaudeKitBadge />
<SponsorButton />
</div>
<div className="flex items-center gap-2">
<ConnectionIndicator />
<DocsLink />
+46
View File
@@ -0,0 +1,46 @@
/**
* Sponsor Button
*
* GitHub Sponsors button for navbar.
* Heart icon with hover animation.
*/
import { Heart } from 'lucide-react';
import { cn } from '@/lib/utils';
const SPONSOR_URL = 'https://github.com/sponsors/kaitranntt';
export function SponsorButton() {
return (
<a
href={SPONSOR_URL}
target="_blank"
rel="noopener noreferrer"
className={cn(
'group inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg',
'bg-pink-500/10 border-2 border-pink-500/40',
'hover:bg-pink-400 hover:border-pink-400',
'transition-all duration-200 shadow-sm hover:shadow-md'
)}
title="Sponsor this project on GitHub"
>
<Heart
className={cn(
'w-4 h-4 text-pink-500',
'group-hover:text-white group-hover:fill-white',
'group-hover:animate-pulse',
'transition-colors'
)}
/>
<span
className={cn(
'text-xs font-bold text-pink-600 dark:text-pink-300',
'group-hover:text-white dark:group-hover:text-white',
'transition-colors'
)}
>
Sponsor
</span>
</a>
);
}
+90
View File
@@ -0,0 +1,90 @@
import * as React from 'react';
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from 'lucide-react';
import { DayPicker } from 'react-day-picker';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button-variants';
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn('p-2', className)}
classNames={{
root: 'relative',
months: 'flex flex-col sm:flex-row gap-2 sm:gap-4',
month: 'flex flex-col gap-2',
month_caption: 'flex justify-center pt-1 relative items-center h-8',
caption_label: 'text-sm font-medium',
nav: 'absolute inset-x-0 top-2 flex items-center justify-between px-1 z-10',
button_previous: cn(
buttonVariants({ variant: 'ghost' }),
'h-7 w-7 bg-transparent p-0 opacity-70 hover:opacity-100 hover:bg-accent rounded-md'
),
button_next: cn(
buttonVariants({ variant: 'ghost' }),
'h-7 w-7 bg-transparent p-0 opacity-70 hover:opacity-100 hover:bg-accent rounded-md'
),
month_grid: 'w-full border-collapse',
weekdays: 'flex w-full',
weekday: 'text-muted-foreground w-8 font-normal text-xs text-center',
weeks: 'flex flex-col',
week: 'flex w-full',
day: cn(
'relative h-8 w-8 p-0 text-center text-xs font-normal focus-within:relative focus-within:z-20',
'hover:bg-accent hover:text-accent-foreground rounded-full select-none'
),
day_button: cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal rounded-full text-xs',
'aria-selected:opacity-100'
),
// Range Selection Styles
range_start: cn(
'aria-selected:bg-primary aria-selected:text-primary-foreground',
'aria-selected:rounded-l-full aria-selected:rounded-r-none',
'aria-selected:hover:bg-primary aria-selected:hover:text-primary-foreground'
),
range_end: cn(
'aria-selected:bg-primary aria-selected:text-primary-foreground',
'aria-selected:rounded-r-full aria-selected:rounded-l-none',
'aria-selected:hover:bg-primary aria-selected:hover:text-primary-foreground'
),
range_middle: cn(
'aria-selected:bg-accent aria-selected:text-accent-foreground',
'aria-selected:rounded-none'
),
selected: cn(
'bg-primary text-primary-foreground rounded-full',
'hover:bg-primary hover:text-primary-foreground',
'focus:bg-primary focus:text-primary-foreground'
),
today: 'bg-accent text-accent-foreground rounded-full',
outside:
'text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
disabled: 'text-muted-foreground opacity-50',
hidden: 'invisible',
...classNames,
}}
components={{
Chevron: ({ orientation, className, ...props }) => {
const Icon =
orientation === 'left'
? ChevronLeft
: orientation === 'right'
? ChevronRight
: orientation === 'up'
? ChevronUp
: ChevronDown;
return <Icon className={cn('h-4 w-4', className)} {...props} />;
},
}}
{...props}
/>
);
}
Calendar.displayName = 'Calendar';
export { Calendar };
+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" />
+15
View File
@@ -23,16 +23,31 @@ export default defineConfig({
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'radix-ui': [
'@radix-ui/react-alert-dialog',
'@radix-ui/react-checkbox',
'@radix-ui/react-collapsible',
'@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu',
'@radix-ui/react-label',
'@radix-ui/react-popover',
'@radix-ui/react-scroll-area',
'@radix-ui/react-select',
'@radix-ui/react-separator',
'@radix-ui/react-slot',
'@radix-ui/react-switch',
'@radix-ui/react-tabs',
'@radix-ui/react-tooltip',
],
'tanstack': ['@tanstack/react-query', '@tanstack/react-table'],
'form-utils': ['react-hook-form', '@hookform/resolvers', 'zod'],
'icons': ['lucide-react'],
// Charts - large library, separate chunk
'charts': ['recharts'],
// Code editor / syntax highlighting
'code-highlight': ['prism-react-renderer'],
// Notifications
'notifications': ['sonner'],
// Utilities
'utils': ['date-fns', 'clsx', 'class-variance-authority', 'tailwind-merge', 'yaml'],
},
},
},