Files
CC-Switch/src/components/usage/UsageSummaryCards.tsx
T
YoVinchen 41267135f5 Feat/auto failover (#367)
* feat(db): add circuit breaker config table and provider proxy target APIs

Add database support for auto-failover feature:

- Add circuit_breaker_config table for storing failover thresholds
- Add get/update_circuit_breaker_config methods in proxy DAO
- Add reset_provider_health method for manual recovery
- Add set_proxy_target and get_proxy_targets methods in providers DAO
  for managing multi-provider failover configuration

* feat(proxy): implement circuit breaker and provider router for auto-failover

Add core failover logic:

- CircuitBreaker: Tracks provider health with three states:
  - Closed: Normal operation, requests pass through
  - Open: Circuit broken after consecutive failures, skip provider
  - HalfOpen: Testing recovery with limited requests
- ProviderRouter: Routes requests across multiple providers with:
  - Health tracking and automatic failover
  - Configurable failure/success thresholds
  - Auto-disable proxy target after reaching failure threshold
  - Support for manual circuit breaker reset
- Export new types in proxy module

* feat(proxy): add failover Tauri commands and integrate with forwarder

Expose failover functionality to frontend:

- Add Tauri commands: get_proxy_targets, set_proxy_target,
  get_provider_health, reset_circuit_breaker,
  get/update_circuit_breaker_config, get_circuit_breaker_stats
- Register all new commands in lib.rs invoke handler
- Update forwarder with improved error handling and logging
- Integrate ProviderRouter with proxy server startup
- Add provider health tracking in request handlers

* feat(frontend): add failover API layer and TanStack Query hooks

Add frontend data layer for failover management:

- Add failover.ts API: Tauri invoke wrappers for all failover commands
- Add failover.ts query hooks: TanStack Query mutations and queries
  - useProxyTargets, useProviderHealth queries
  - useSetProxyTarget, useResetCircuitBreaker mutations
  - useCircuitBreakerConfig query and mutation
- Update queries.ts with provider health query key
- Update mutations.ts to invalidate health on provider changes
- Add CircuitBreakerConfig and ProviderHealth types

* feat(ui): add auto-failover configuration UI and provider health display

Add comprehensive UI for failover management:

Components:
- ProviderHealthBadge: Display provider health status with color coding
- CircuitBreakerConfigPanel: Configure failure/success thresholds,
  timeout duration, and error rate limits
- AutoFailoverConfigPanel: Manage proxy targets with drag-and-drop
  priority ordering and individual enable/disable controls
- ProxyPanel: Integrate failover tabs for unified proxy management

Provider enhancements:
- ProviderCard: Show health badge and proxy target indicator
- ProviderActions: Add "Set as Proxy Target" action
- EditProviderDialog: Add is_proxy_target toggle
- ProviderList: Support proxy target filtering mode

Other:
- Update App.tsx routing for settings integration
- Update useProviderActions hook with proxy target mutation
- Fix ProviderList tests for updated component API

* fix(usage): stabilize date range to prevent infinite re-renders

* feat(backend): add tool version check command

Add get_tool_versions command to check local and latest versions of
Claude, Codex, and Gemini CLI tools:

- Detect local installed versions via command line execution
- Fetch latest versions from npm registry (Claude, Gemini)
  and GitHub releases API (Codex)
- Return comprehensive version info including error details
  for uninstalled tools
- Register command in Tauri invoke handler

* style(ui): format accordion component code style

Apply consistent code formatting to accordion component:
- Convert double quotes to semicolons at line endings
- Adjust indentation to 2-space standard
- Align with project code style conventions

* refactor(providers): update provider card styling to use theme tokens

Replace hardcoded color classes with semantic design tokens:
- Use bg-card, border-border, text-card-foreground instead of glass-card
- Replace gray/white color literals with muted/foreground tokens
- Change proxy target indicator color from purple to green
- Improve hover states with border-border-active
- Ensure consistent dark mode support via CSS variables

* refactor(proxy): simplify auto-failover config panel structure

Restructure AutoFailoverConfigPanel for better integration:
- Remove internal Card wrapper and expansion toggle (now handled by parent)
- Extract enabled state to props for external control
- Simplify loading state display
- Clean up redundant CardHeader/CardContent wrappers
- ProxyPanel: reduce complexity by delegating to parent components

* feat(settings): enhance settings page with accordion layout and tool versions

Major settings page improvements:

AboutSection:
- Add local tool version detection (Claude, Codex, Gemini)
- Display installed vs latest version comparison with visual indicators
- Show update availability badges and environment check cards

SettingsPage:
- Reorganize advanced settings into collapsible accordion sections
- Add proxy control panel with inline status toggle
- Integrate auto-failover configuration with accordion UI
- Add database and cost calculation config sections

DirectorySettings & WindowSettings:
- Minor styling adjustments for consistency

settings.ts API:
- Add getToolVersions() wrapper for new backend command

* refactor(usage): restructure usage dashboard components

Comprehensive usage statistics panel refactoring:

UsageDashboard:
- Reorganize layout with improved section headers
- Add better loading states and empty state handling

ModelStatsTable & ProviderStatsTable:
- Minor styling updates for consistency

ModelTestConfigPanel & PricingConfigPanel:
- Simplify component structure
- Remove redundant Card wrappers
- Improve form field organization

RequestLogTable:
- Enhance table layout with better column sizing
- Improve pagination controls

UsageSummaryCards:
- Update card styling with semantic tokens
- Better responsive grid layout

UsageTrendChart:
- Refine chart container styling
- Improve legend and tooltip display

* chore(deps): add accordion and animation dependencies

Package updates:
- Add @radix-ui/react-accordion for collapsible sections
- Add cmdk for command palette support
- Add framer-motion for enhanced animations

Tailwind config:
- Add accordion-up/accordion-down animations
- Update darkMode config to support both selector and class
- Reorganize color and keyframe definitions for clarity

* style(app): update header and app switcher styling

App.tsx:
- Replace glass-header with explicit bg-background/80 backdrop-blur
- Update navigation button container to use bg-muted

AppSwitcher:
- Replace hardcoded gray colors with semantic muted/foreground tokens
- Ensure consistent dark mode support via CSS variables
- Add group class for better hover state transitions
2025-12-08 21:14:06 +08:00

170 lines
5.5 KiB
TypeScript

import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { useUsageSummary } from "@/lib/query/usage";
import { Activity, DollarSign, Layers, Database, Loader2 } from "lucide-react";
import { motion } from "framer-motion";
interface UsageSummaryCardsProps {
days: number;
}
export function UsageSummaryCards({ days }: UsageSummaryCardsProps) {
const { t } = useTranslation();
const { startDate, endDate } = useMemo(() => {
const end = Math.floor(Date.now() / 1000);
const start = end - days * 24 * 60 * 60;
return { startDate: start, endDate: end };
}, [days]);
const { data: summary, isLoading } = useUsageSummary(startDate, endDate);
const stats = useMemo(() => {
const totalRequests = summary?.totalRequests ?? 0;
const totalCost = parseFloat(summary?.totalCost || "0");
const inputTokens = summary?.totalInputTokens ?? 0;
const outputTokens = summary?.totalOutputTokens ?? 0;
const totalTokens = inputTokens + outputTokens;
const cacheWriteTokens = summary?.totalCacheCreationTokens ?? 0;
const cacheReadTokens = summary?.totalCacheReadTokens ?? 0;
const totalCacheTokens = cacheWriteTokens + cacheReadTokens;
return [
{
title: t("usage.totalRequests", "总请求数"),
value: totalRequests.toLocaleString(),
icon: Activity,
color: "text-blue-500",
bg: "bg-blue-500/10",
subValue: null,
},
{
title: t("usage.totalCost", "总成本"),
value: `$${totalCost.toFixed(4)}`,
icon: DollarSign,
color: "text-green-500",
bg: "bg-green-500/10",
subValue: null,
},
{
title: t("usage.totalTokens", "总 Token 数"),
value: totalTokens.toLocaleString(),
icon: Layers,
color: "text-purple-500",
bg: "bg-purple-500/10",
subValue: (
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
<div className="flex justify-between items-center">
<span>Input</span>
<span className="text-foreground/80">
{(inputTokens / 1000).toFixed(1)}k
</span>
</div>
<div className="flex justify-between items-center">
<span>Output</span>
<span className="text-foreground/80">
{(outputTokens / 1000).toFixed(1)}k
</span>
</div>
</div>
),
},
{
title: t("usage.cacheTokens", "缓存 Token"),
value: totalCacheTokens.toLocaleString(),
icon: Database,
color: "text-orange-500",
bg: "bg-orange-500/10",
subValue: (
<div className="flex flex-col gap-1 text-xs text-muted-foreground mt-3 pt-3 border-t border-border/50">
<div className="flex justify-between items-center">
<span>Write</span>
<span className="text-foreground/80">
{(cacheWriteTokens / 1000).toFixed(1)}k
</span>
</div>
<div className="flex justify-between items-center">
<span>Read</span>
<span className="text-foreground/80">
{(cacheReadTokens / 1000).toFixed(1)}k
</span>
</div>
</div>
),
},
];
}, [summary, t]);
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
if (isLoading) {
return (
<div className="grid gap-4 md:grid-cols-4">
{[...Array(4)].map((_, i) => (
<Card
key={i}
className="border border-border/50 bg-card/40 backdrop-blur-sm shadow-sm"
>
<CardContent className="p-6 flex items-center justify-center min-h-[160px]">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground/50" />
</CardContent>
</Card>
))}
</div>
);
}
return (
<motion.div
variants={container}
initial="hidden"
animate="show"
className="grid gap-4 md:grid-cols-4"
>
{stats.map((stat, i) => (
<motion.div key={i} variants={item}>
<Card className="relative h-full overflow-hidden border border-border/50 bg-gradient-to-br from-card/50 to-background/50 backdrop-blur-xl hover:from-card/60 hover:to-background/60 transition-all shadow-sm">
<CardContent className="p-5">
<div className="flex items-start justify-between mb-2">
<p className="text-sm font-medium text-muted-foreground">
{stat.title}
</p>
<div className={`p-2 rounded-lg ${stat.bg}`}>
<stat.icon className={`h-4 w-4 ${stat.color}`} />
</div>
</div>
<div className="space-y-1">
<h3 className="text-2xl font-bold truncate" title={stat.value}>
{stat.value}
</h3>
</div>
{stat.subValue || (
/* Placeholder to properly align cards if no subvalue (first 2 cards) - effectively adding empty space or using flex-1 equivalent */
<div className="mt-3 pt-3 border-t border-transparent h-[52px]"></div>
)}
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
);
}