diff --git a/web/src/components/layout/app-layout.tsx b/web/src/components/layout/app-layout.tsx index 00eb09b6..5560a4a2 100644 --- a/web/src/components/layout/app-layout.tsx +++ b/web/src/components/layout/app-layout.tsx @@ -13,14 +13,14 @@ export function AppLayout() { const timeoutSeconds = parseInt(settings?.force_project_timeout || '30', 10); return ( - + - + {/* Mobile header with sidebar trigger */} -
+
-
+
diff --git a/web/src/components/layout/app-sidebar/animated-nav-item.tsx b/web/src/components/layout/app-sidebar/animated-nav-item.tsx new file mode 100644 index 00000000..ea467a61 --- /dev/null +++ b/web/src/components/layout/app-sidebar/animated-nav-item.tsx @@ -0,0 +1,60 @@ +import { NavLink, useLocation } from 'react-router-dom'; +import { StreamingBadge } from '@/components/ui/streaming-badge'; +import { MarqueeBackground } from '@/components/ui/marquee-background'; +import { SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; +import { cn } from '@/lib/utils'; +import type { ReactNode } from 'react'; + +interface AnimatedNavItemProps { + /** The route path to navigate to */ + to: string; + /** Function to check if the route is active */ + isActive: (pathname: string) => boolean; + /** Tooltip text */ + tooltip: string; + /** Icon element */ + icon: ReactNode; + /** Label text */ + label: string; + /** Streaming count for badge */ + streamingCount: number; + /** Color for marquee and badge */ + color: string; +} + +/** + * Reusable navigation item with marquee background and streaming badge + */ +export function AnimatedNavItem({ + to, + isActive: isActiveFn, + tooltip, + icon, + label, + streamingCount, + color, +}: AnimatedNavItemProps) { + const location = useLocation(); + const isActive = isActiveFn(location.pathname); + + return ( + + } + isActive={isActive} + tooltip={tooltip} + className={cn( + 'relative overflow-hidden', + isActive && 'bg-transparent! hover:bg-sidebar-accent/50!', + )} + > + 0} color={color} opacity={0.3} /> + {icon} + {label} + + + + + + ); +} diff --git a/web/src/components/layout/app-sidebar/client-routes-items.tsx b/web/src/components/layout/app-sidebar/client-routes-items.tsx index 5d52aaf5..387a62e0 100644 --- a/web/src/components/layout/app-sidebar/client-routes-items.tsx +++ b/web/src/components/layout/app-sidebar/client-routes-items.tsx @@ -1,44 +1,33 @@ -import { NavLink, useLocation } from 'react-router-dom'; import { ClientIcon, allClientTypes, getClientName, getClientColor, } from '@/components/icons/client-icons'; -import { StreamingBadge } from '@/components/ui/streaming-badge'; -import { MarqueeBackground } from '@/components/ui/marquee-background'; import { useStreamingRequests } from '@/hooks/use-streaming'; import type { ClientType } from '@/lib/transport'; -import { SidebarMenuButton, SidebarMenuItem, SidebarMenuBadge } from '@/components/ui/sidebar'; +import { AnimatedNavItem } from './animated-nav-item'; function ClientNavItem({ clientType, - streamingCount + streamingCount, }: { clientType: ClientType; streamingCount: number; }) { - const location = useLocation(); const color = getClientColor(clientType); const clientName = getClientName(clientType); - const isActive = location.pathname === `/routes/${clientType}`; return ( - - } - isActive={isActive} - tooltip={clientName} - className="relative overflow-hidden" - > - 0 && !isActive} color={color} opacity={0.5} /> - - {clientName} - - - - - + pathname === `/routes/${clientType}`} + tooltip={clientName} + icon={} + label={clientName} + streamingCount={streamingCount} + color={color} + /> ); } diff --git a/web/src/components/layout/app-sidebar/index.tsx b/web/src/components/layout/app-sidebar/index.tsx index 7a6293ad..1425f028 100644 --- a/web/src/components/layout/app-sidebar/index.tsx +++ b/web/src/components/layout/app-sidebar/index.tsx @@ -16,7 +16,7 @@ export function AppSidebar() { const versionDisplay = proxyStatus?.version ?? '...'; return ( - + @@ -37,6 +37,3 @@ export function AppSidebar() { ); } - -// Alias for backwards compatibility -export { AppSidebar as SidebarNav }; diff --git a/web/src/components/layout/app-sidebar/requests-nav-item.tsx b/web/src/components/layout/app-sidebar/requests-nav-item.tsx index 1ad03595..1097569a 100644 --- a/web/src/components/layout/app-sidebar/requests-nav-item.tsx +++ b/web/src/components/layout/app-sidebar/requests-nav-item.tsx @@ -1,37 +1,25 @@ -import { NavLink, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Activity } from 'lucide-react'; -import { StreamingBadge } from '@/components/ui/streaming-badge'; -import { MarqueeBackground } from '@/components/ui/marquee-background'; import { useStreamingRequests } from '@/hooks/use-streaming'; -import { SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; +import { AnimatedNavItem } from './animated-nav-item'; /** * Requests navigation item with streaming badge and marquee animation */ export function RequestsNavItem() { - const location = useLocation(); const { total } = useStreamingRequests(); const { t } = useTranslation(); - const isActive = - location.pathname === '/requests' || location.pathname.startsWith('/requests/'); const color = 'var(--color-success)'; // emerald-500 return ( - - } - isActive={isActive} - tooltip={t('requests.title')} - className="relative" - > - 0 && !isActive} color={color} opacity={0.4} /> - - {t('requests.title')} - - - - - + pathname === '/requests' || pathname.startsWith('/requests/')} + tooltip={t('requests.title')} + icon={} + label={t('requests.title')} + streamingCount={total} + color={color} + /> ); } diff --git a/web/src/components/layout/index.ts b/web/src/components/layout/index.ts index cf6b938c..3d902b34 100644 --- a/web/src/components/layout/index.ts +++ b/web/src/components/layout/index.ts @@ -1,5 +1,5 @@ export { AppLayout } from './app-layout'; -export { SidebarNav } from './app-sidebar'; +export { AppSidebar } from './app-sidebar'; export { PageHeader } from './page-header'; export { NavProxyStatus } from './nav-proxy-status'; export { SidebarRenderer } from './app-sidebar/sidebar-renderer'; diff --git a/web/src/components/layout/nav-proxy-status.tsx b/web/src/components/layout/nav-proxy-status.tsx index ac025ed3..99c83b26 100644 --- a/web/src/components/layout/nav-proxy-status.tsx +++ b/web/src/components/layout/nav-proxy-status.tsx @@ -3,7 +3,6 @@ import { Radio, Check, Copy } from 'lucide-react'; import { useProxyStatus } from '@/hooks/queries'; import { useSidebar } from '@/components/ui/sidebar'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import { Button } from '../ui'; import { useTranslation } from 'react-i18next'; export function NavProxyStatus() { @@ -60,20 +59,20 @@ export function NavProxyStatus() { } return ( -
- + + ); } diff --git a/web/src/components/routes/ClientTypeRoutesContent.tsx b/web/src/components/routes/ClientTypeRoutesContent.tsx index 4758a3a7..1f4b0e72 100644 --- a/web/src/components/routes/ClientTypeRoutesContent.tsx +++ b/web/src/components/routes/ClientTypeRoutesContent.tsx @@ -56,9 +56,9 @@ export function ClientTypeRoutesContent({ projectID, searchQuery = '', }: ClientTypeRoutesContentProps) { - const [activeId, setActiveId] = useState(null) - const { data: providerStats = {} } = useProviderStats(clientType, projectID || undefined) - const queryClient = useQueryClient() + const [activeId, setActiveId] = useState(null); + const { data: providerStats = {} } = useProviderStats(clientType, projectID || undefined); + const queryClient = useQueryClient(); const sensors = useSensors( useSensor(PointerSensor, { @@ -107,9 +107,10 @@ export function ClientTypeRoutesContent({ // Apply search filter if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); - filteredItems = filteredItems.filter((item) => - item.provider.name.toLowerCase().includes(query) || - item.provider.type.toLowerCase().includes(query) + filteredItems = filteredItems.filter( + (item) => + item.provider.name.toLowerCase().includes(query) || + item.provider.type.toLowerCase().includes(query), ); } @@ -133,9 +134,8 @@ export function ClientTypeRoutesContent({ // Apply search filter if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); - available = available.filter((p) => - p.name.toLowerCase().includes(query) || - p.type.toLowerCase().includes(query) + available = available.filter( + (p) => p.name.toLowerCase().includes(query) || p.type.toLowerCase().includes(query), ); } @@ -241,131 +241,132 @@ export function ClientTypeRoutesContent({
{/* Routes List */} {items.length > 0 ? ( - - item.id)} - strategy={verticalListSortingStrategy} + -
- {items.map((item, index) => ( - item.id)} + strategy={verticalListSortingStrategy} + > +
+ {items.map((item, index) => ( + handleToggle(item)} + onDelete={item.route ? () => handleDeleteRoute(item.route!.id) : undefined} + /> + ))} +
+ + + + {activeItem && ( + i.id === activeItem.id)} clientType={clientType} streamingCount={ - countsByProviderAndClient.get(`${item.provider.id}:${clientType}`) || 0 + countsByProviderAndClient.get(`${activeItem.provider.id}:${clientType}`) || + 0 } - stats={providerStats[item.provider.id]} - isToggling={toggleRoute.isPending || createRoute.isPending} - onToggle={() => handleToggle(item)} - onDelete={item.route ? () => handleDeleteRoute(item.route!.id) : undefined} + stats={providerStats[activeItem.provider.id]} + isToggling={false} + isOverlay + onToggle={() => {}} /> - ))} -
-
- - - {activeItem && ( - i.id === activeItem.id)} - clientType={clientType} - streamingCount={ - countsByProviderAndClient.get(`${activeItem.provider.id}:${clientType}`) || 0 - } - stats={providerStats[activeItem.provider.id]} - isToggling={false} - isOverlay - onToggle={() => {}} - /> - )} - -
- ) : ( -
-

No routes configured for {getClientName(clientType)}

-

Add a route below to get started

-
- )} - - {/* Add Route Section - Card Style */} - {availableProviders.length > 0 && ( -
-
- - - Available Providers - + )} + + + ) : ( +
+

No routes configured for {getClientName(clientType)}

+

Add a route below to get started

-
- {availableProviders.map((provider) => { - const isNative = (provider.supportedClientTypes || []).includes(clientType); - const providerColor = getProviderColor(provider.type as ProviderType); - return ( -
- {/* Right: Add Icon */} - - - ); - })} + {/* Right: Add Icon */} + + + ); + })} +
-
- )} + )} + - - + ); } diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx index 3da1a9ca..1a732859 100644 --- a/web/src/components/ui/button.tsx +++ b/web/src/components/ui/button.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none cursor-pointer shadow-xs", + "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", { variants: { variant: { diff --git a/web/src/components/ui/model-input.tsx b/web/src/components/ui/model-input.tsx index d1c581ba..3d29fb93 100644 --- a/web/src/components/ui/model-input.tsx +++ b/web/src/components/ui/model-input.tsx @@ -68,7 +68,11 @@ const COMMON_MODELS = [ { id: 'qwen/qwen2.5-coder-32b-instruct', name: 'Qwen 2.5 Coder 32B', provider: 'NVIDIA' }, { id: 'openai/gpt-oss-120b', name: 'GPT OSS 120B', provider: 'NVIDIA' }, { id: 'google/gemma-3-27b-it', name: 'Gemma 3 27B', provider: 'NVIDIA' }, - { id: 'meta/llama-4-maverick-17b-128e-instruct', name: 'Llama 4 Maverick 17B', provider: 'NVIDIA' }, + { + id: 'meta/llama-4-maverick-17b-128e-instruct', + name: 'Llama 4 Maverick 17B', + provider: 'NVIDIA', + }, { id: 'mistralai/devstral-2-123b-instruct-2512', name: 'Devstral 2 123B', provider: 'NVIDIA' }, // Antigravity supported target models (use these as mapping targets) { diff --git a/web/src/components/ui/tabs.tsx b/web/src/components/ui/tabs.tsx index 3fec598a..ea7244ed 100644 --- a/web/src/components/ui/tabs.tsx +++ b/web/src/components/ui/tabs.tsx @@ -8,7 +8,7 @@ function Tabs({ className, orientation = 'horizontal', ...props }: TabsPrimitive ); @@ -64,7 +64,7 @@ function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) { return ( ); diff --git a/web/src/contexts/antigravity-quotas-context.tsx b/web/src/contexts/antigravity-quotas-context.tsx index 1a349dbb..5e78c23c 100644 --- a/web/src/contexts/antigravity-quotas-context.tsx +++ b/web/src/contexts/antigravity-quotas-context.tsx @@ -20,7 +20,10 @@ interface AntigravityQuotasProviderProps { enabled?: boolean; } -export function AntigravityQuotasProvider({ children, enabled = true }: AntigravityQuotasProviderProps) { +export function AntigravityQuotasProvider({ + children, + enabled = true, +}: AntigravityQuotasProviderProps) { const { data: quotas, isLoading } = useAntigravityBatchQuotas(enabled); const getQuotaForProvider = (providerId: number): AntigravityQuotaData | undefined => { @@ -43,7 +46,9 @@ export function useAntigravityQuotasContext() { } // 可选的 hook,用于在没有 Provider 时不抛出错误 -export function useAntigravityQuotaFromContext(providerId: number): AntigravityQuotaData | undefined { +export function useAntigravityQuotaFromContext( + providerId: number, +): AntigravityQuotaData | undefined { const context = useContext(AntigravityQuotasContext); return context?.getQuotaForProvider(providerId); } diff --git a/web/src/hooks/queries/use-aggregated-stats.ts b/web/src/hooks/queries/use-aggregated-stats.ts index edaafa99..df6bd818 100644 --- a/web/src/hooks/queries/use-aggregated-stats.ts +++ b/web/src/hooks/queries/use-aggregated-stats.ts @@ -5,7 +5,12 @@ import { useMemo } from 'react'; import { useUsageStats, getTimeRange, type TimeRangePreset } from './use-usage-stats'; -import type { UsageStatsFilter, ProviderStats, UsageStats, StatsGranularity } from '@/lib/transport'; +import type { + UsageStatsFilter, + ProviderStats, + UsageStats, + StatsGranularity, +} from '@/lib/transport'; // Route 统计数据类型 export interface RouteStats { diff --git a/web/src/hooks/queries/use-usage-stats.ts b/web/src/hooks/queries/use-usage-stats.ts index 86e1dea7..7fbf445a 100644 --- a/web/src/hooks/queries/use-usage-stats.ts +++ b/web/src/hooks/queries/use-usage-stats.ts @@ -125,7 +125,7 @@ export function useUsageStats(filter?: UsageStatsFilter) { */ export function useUsageStatsWithPreset( preset: TimeRangePreset, - additionalFilter?: Omit + additionalFilter?: Omit, ) { const { start, end, granularity } = getTimeRange(preset); diff --git a/web/src/index.css b/web/src/index.css index 0664a7d8..e3083d3b 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -249,11 +249,6 @@ --color-client-gemini: var(--client-gemini); } -/* ===== Base Styles ===== */ -* { - border-color: var(--color-border); -} - body { background-color: var(--color-background); color: var(--color-foreground); @@ -293,9 +288,10 @@ body { /* ===== Component Styles ===== */ html, -body { +body, +#root { width: 100%; - height: 100%; + height: 100svh; margin: 0; padding: 0; } @@ -612,4 +608,9 @@ body { body { letter-spacing: var(--tracking-normal); } + + button, + [role='button'] { + cursor: pointer; + } } diff --git a/web/src/lib/transport/types.ts b/web/src/lib/transport/types.ts index efca02f1..a449d122 100644 --- a/web/src/lib/transport/types.ts +++ b/web/src/lib/transport/types.ts @@ -381,18 +381,18 @@ export interface ModelMapping { id: number; createdAt: string; updatedAt: string; - scope: ModelMappingScope; // 作用域类型 - clientType: string; // 客户端类型,空表示所有 - providerType: string; // 供应商类型(如 antigravity, kiro, custom),空表示所有 - providerID: number; // 供应商 ID,0 表示所有 - projectID: number; // 项目 ID,0 表示所有 - routeID: number; // 路由 ID,0 表示所有 - apiTokenID: number; // Token ID,0 表示所有 - pattern: string; // 源模式,支持 * 通配符 - target: string; // 目标模型名 - priority: number; // 优先级,数字越小优先级越高 - isEnabled: boolean; // 是否启用 - isBuiltin: boolean; // 是否为内置规则 + scope: ModelMappingScope; // 作用域类型 + clientType: string; // 客户端类型,空表示所有 + providerType: string; // 供应商类型(如 antigravity, kiro, custom),空表示所有 + providerID: number; // 供应商 ID,0 表示所有 + projectID: number; // 项目 ID,0 表示所有 + routeID: number; // 路由 ID,0 表示所有 + apiTokenID: number; // Token ID,0 表示所有 + pattern: string; // 源模式,支持 * 通配符 + target: string; // 目标模型名 + priority: number; // 优先级,数字越小优先级越高 + isEnabled: boolean; // 是否启用 + isBuiltin: boolean; // 是否为内置规则 } // 创建/更新模型映射的请求 @@ -527,18 +527,18 @@ export type StatsGranularity = 'minute' | 'hour' | 'day' | 'week' | 'month'; export interface UsageStats { id: number; createdAt: string; - timeBucket: string; // 时间桶(根据粒度截断) + timeBucket: string; // 时间桶(根据粒度截断) granularity: StatsGranularity; // 时间粒度 routeID: number; providerID: number; projectID: number; apiTokenID: number; clientType: string; - model: string; // 请求的模型名称 + model: string; // 请求的模型名称 totalRequests: number; successfulRequests: number; failedRequests: number; - totalDurationMs: number; // 累计请求耗时(毫秒) + totalDurationMs: number; // 累计请求耗时(毫秒) inputTokens: number; outputTokens: number; cacheRead: number; @@ -551,24 +551,24 @@ export interface UsageStatsSummary { totalRequests: number; successfulRequests: number; failedRequests: number; - successRate: number; // 0-100 + successRate: number; // 0-100 totalInputTokens: number; totalOutputTokens: number; totalCacheRead: number; totalCacheWrite: number; - totalCost: number; // 微美元 + totalCost: number; // 微美元 } export interface UsageStatsFilter { granularity?: StatsGranularity; // 时间粒度(必填) - start?: string; // 开始时间 ISO8601 - end?: string; // 结束时间 ISO8601 + start?: string; // 开始时间 ISO8601 + end?: string; // 结束时间 ISO8601 routeId?: number; providerId?: number; projectId?: number; apiTokenId?: number; clientType?: string; - model?: string; // 模型名称 + model?: string; // 模型名称 } /** Response Model - 记录所有出现过的 response model */ diff --git a/web/src/pages/client-routes/components/provider-row.tsx b/web/src/pages/client-routes/components/provider-row.tsx index ae9619ae..ba52f1f8 100644 --- a/web/src/pages/client-routes/components/provider-row.tsx +++ b/web/src/pages/client-routes/components/provider-row.tsx @@ -235,20 +235,21 @@ export function ProviderRowContent({ variant={null} onClick={handleContentClick} className={cn( - 'group relative flex items-center gap-4 p-3 rounded-xl border transition-all duration-300 overflow-hidden w-full h-auto', + 'group relative flex items-center gap-4 p-3 rounded-xl border transition-all duration-300 overflow-hidden w-full h-auto cursor-grab active:cursor-grabbing', isInCooldown - ? 'bg-teal-200/70 dark:bg-teal-950/80 border-teal-400/60 shadow-[0_0_25px_rgba(20,184,166,0.3)] cursor-pointer' + ? 'bg-teal-200/70 dark:bg-teal-950/80 border-teal-400/60 shadow-[0_0_25px_rgba(20,184,166,0.3)]' : enabled ? streamingCount > 0 ? 'bg-accent/5 border-transparent ring-1 ring-black/5 dark:ring-white/10' - : 'bg-card/60 border-border hover:border-emerald-500/30 hover:bg-card shadow-sm cursor-pointer' - : 'bg-muted/40 border-dashed border-border opacity-70 cursor-pointer grayscale-[0.5] hover:opacity-100 hover:grayscale-0', + : 'bg-card/60 border-border hover:border-emerald-500/30 hover:bg-card shadow-sm' + : 'bg-muted/40 border-dashed border-border opacity-70 grayscale-[0.5] hover:opacity-100 hover:grayscale-0', )} style={{ borderColor: !isInCooldown && enabled && streamingCount > 0 ? `${color}40` : undefined, boxShadow: !isInCooldown && enabled && streamingCount > 0 ? `0 0 20px ${color}15` : undefined, }} + {...dragHandleListeners} > 0 && enabled && !isInCooldown} @@ -269,10 +270,7 @@ export function ProviderRowContent({ {/* Drag Handle & Index */}
-
+
- Claude + + Claude +
!m.scope || m.scope === 'global') + const rules = (mappings || []).filter((m) => !m.scope || m.scope === 'global'); const sensors = useSensors( useSensor(PointerSensor), diff --git a/web/src/pages/providers/components/antigravity-provider-view.tsx b/web/src/pages/providers/components/antigravity-provider-view.tsx index 7073e02a..a6c0624f 100644 --- a/web/src/pages/providers/components/antigravity-provider-view.tsx +++ b/web/src/pages/providers/components/antigravity-provider-view.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from 'react' +import { useState, useEffect, useMemo } from 'react'; import { Wand2, Mail, @@ -10,26 +10,26 @@ import { Plus, ArrowRight, Zap, -} from 'lucide-react' -import { useTranslation } from 'react-i18next' -import { ClientIcon } from '@/components/icons/client-icons' +} from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { ClientIcon } from '@/components/icons/client-icons'; import type { Provider, AntigravityQuotaData, AntigravityModelQuota, ModelMapping, ModelMappingInput, -} from '@/lib/transport' -import { getTransport } from '@/lib/transport' +} from '@/lib/transport'; +import { getTransport } from '@/lib/transport'; import { useModelMappings, useCreateModelMapping, useUpdateModelMapping, useDeleteModelMapping, -} from '@/hooks/queries' -import { Button } from '@/components/ui' -import { ModelInput } from '@/components/ui/model-input' -import { ANTIGRAVITY_COLOR } from '../types' +} from '@/hooks/queries'; +import { Button } from '@/components/ui'; +import { ModelInput } from '@/components/ui/model-input'; +import { ANTIGRAVITY_COLOR } from '../types'; interface AntigravityProviderViewProps { provider: Provider; @@ -128,25 +128,25 @@ function ModelQuotaCard({ model }: { model: AntigravityModelQuota }) { // Provider Model Mappings Section function ProviderModelMappings({ provider }: { provider: Provider }) { - const { t } = useTranslation() - const { data: allMappings } = useModelMappings() - const createMapping = useCreateModelMapping() - const updateMapping = useUpdateModelMapping() - const deleteMapping = useDeleteModelMapping() - const [newPattern, setNewPattern] = useState('') - const [newTarget, setNewTarget] = useState('') + const { t } = useTranslation(); + const { data: allMappings } = useModelMappings(); + const createMapping = useCreateModelMapping(); + const updateMapping = useUpdateModelMapping(); + const deleteMapping = useDeleteModelMapping(); + const [newPattern, setNewPattern] = useState(''); + const [newTarget, setNewTarget] = useState(''); // Filter mappings for this provider const providerMappings = useMemo(() => { return (allMappings || []).filter( - m => m.scope === 'provider' && m.providerID === provider.id - ) - }, [allMappings, provider.id]) + (m) => m.scope === 'provider' && m.providerID === provider.id, + ); + }, [allMappings, provider.id]); - const isPending = createMapping.isPending || updateMapping.isPending || deleteMapping.isPending + const isPending = createMapping.isPending || updateMapping.isPending || deleteMapping.isPending; const handleAddMapping = async () => { - if (!newPattern.trim() || !newTarget.trim()) return + if (!newPattern.trim() || !newTarget.trim()) return; await createMapping.mutateAsync({ pattern: newPattern.trim(), @@ -156,10 +156,10 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { providerType: 'antigravity', priority: providerMappings.length * 10 + 1000, isEnabled: true, - }) - setNewPattern('') - setNewTarget('') - } + }); + setNewPattern(''); + setNewTarget(''); + }; const handleUpdateMapping = async (mapping: ModelMapping, data: Partial) => { await updateMapping.mutateAsync({ @@ -173,29 +173,23 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { priority: mapping.priority, isEnabled: mapping.isEnabled, }, - }) - } + }); + }; const handleDeleteMapping = async (id: number) => { - await deleteMapping.mutateAsync(id) - } + await deleteMapping.mutateAsync(id); + }; return (
-

- {t('modelMappings.title')} -

- - ({providerMappings.length}) - +

{t('modelMappings.title')}

+ ({providerMappings.length})
-

- {t('modelMappings.pageDesc')} -

+

{t('modelMappings.pageDesc')}

{providerMappings.length > 0 && (
@@ -204,7 +198,7 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { {index + 1}. handleUpdateMapping(mapping, { pattern })} + onChange={(pattern) => handleUpdateMapping(mapping, { pattern })} placeholder={t('modelMappings.matchPattern')} disabled={isPending} className="flex-1 min-w-0 h-8 text-sm" @@ -212,7 +206,7 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { handleUpdateMapping(mapping, { target })} + onChange={(target) => handleUpdateMapping(mapping, { target })} placeholder={t('modelMappings.targetModel')} disabled={isPending} className="flex-1 min-w-0 h-8 text-sm" @@ -264,7 +258,7 @@ function ProviderModelMappings({ provider }: { provider: Provider }) {
- ) + ); } export function AntigravityProviderView({ diff --git a/web/src/pages/providers/components/antigravity-token-import.tsx b/web/src/pages/providers/components/antigravity-token-import.tsx index fad6d09e..8f647c09 100644 --- a/web/src/pages/providers/components/antigravity-token-import.tsx +++ b/web/src/pages/providers/components/antigravity-token-import.tsx @@ -253,26 +253,26 @@ export function AntigravityTokenImport() { className={cn( 'relative group p-4 rounded-xl border-2 transition-all duration-200 text-left', mode === 'oauth' - ? 'border-accent bg-accent/5 shadow-sm' - : 'border-border hover:border-accent/50 bg-muted hover:bg-accent', + ? 'border-primary bg-primary/10 shadow-md' + : 'border-border hover:border-primary/30 bg-secondary/50 hover:bg-secondary', )} >
-
+
OAuth Connect @@ -283,7 +283,7 @@ export function AntigravityTokenImport() {
{mode === 'oauth' && ( -
+
)} @@ -292,26 +292,26 @@ export function AntigravityTokenImport() { className={cn( 'relative group p-4 rounded-xl border-2 transition-all duration-200 text-left', mode === 'token' - ? 'border-accent bg-accent/5 shadow-sm' - : 'border-border hover:border-accent/50 bg-muted hover:bg-accent', + ? 'border-primary bg-primary/10 shadow-md' + : 'border-border hover:border-primary/30 bg-secondary/50 hover:bg-secondary', )} >
-
+
Manual Token @@ -322,7 +322,7 @@ export function AntigravityTokenImport() {
{mode === 'token' && ( -
+
)}
diff --git a/web/src/pages/providers/components/custom-config-step.tsx b/web/src/pages/providers/components/custom-config-step.tsx index b24c56fc..4a052591 100644 --- a/web/src/pages/providers/components/custom-config-step.tsx +++ b/web/src/pages/providers/components/custom-config-step.tsx @@ -11,7 +11,16 @@ import { useProviderNavigation } from '../hooks/use-provider-navigation'; export function CustomConfigStep() { const { t } = useTranslation(); - const { formData, updateFormData, updateClient, isValid, isSaving, setSaving, saveStatus, setSaveStatus } = useProviderForm(); + const { + formData, + updateFormData, + updateClient, + isValid, + isSaving, + setSaving, + saveStatus, + setSaveStatus, + } = useProviderForm(); const { goToSelectType, goToProviders } = useProviderNavigation(); const createProvider = useCreateProvider(); const createModelMapping = useCreateModelMapping(); @@ -179,7 +188,10 @@ export function CustomConfigStep() { variant="outline" size="sm" onClick={() => { - const newMappings = [...(formData.modelMappings || []), { pattern: '', target: '' }]; + const newMappings = [ + ...(formData.modelMappings || []), + { pattern: '', target: '' }, + ]; updateFormData({ modelMappings: newMappings }); }} > @@ -228,7 +240,9 @@ export function CustomConfigStep() { size="icon" className="shrink-0 mt-5 text-muted-foreground hover:text-destructive" onClick={() => { - const newMappings = (formData.modelMappings || []).filter((_, i) => i !== index); + const newMappings = (formData.modelMappings || []).filter( + (_, i) => i !== index, + ); updateFormData({ modelMappings: newMappings }); }} > diff --git a/web/src/pages/providers/components/kiro-provider-view.tsx b/web/src/pages/providers/components/kiro-provider-view.tsx index 79eefa8b..926fba40 100644 --- a/web/src/pages/providers/components/kiro-provider-view.tsx +++ b/web/src/pages/providers/components/kiro-provider-view.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from 'react' +import { useState, useEffect, useMemo } from 'react'; import { Zap, Mail, @@ -9,25 +9,20 @@ import { AlertTriangle, Plus, ArrowRight, -} from 'lucide-react' -import { useTranslation } from 'react-i18next' -import { ClientIcon } from '@/components/icons/client-icons' -import type { - Provider, - KiroQuotaData, - ModelMapping, - ModelMappingInput, -} from '@/lib/transport' -import { getTransport } from '@/lib/transport' +} from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { ClientIcon } from '@/components/icons/client-icons'; +import type { Provider, KiroQuotaData, ModelMapping, ModelMappingInput } from '@/lib/transport'; +import { getTransport } from '@/lib/transport'; import { useModelMappings, useCreateModelMapping, useUpdateModelMapping, useDeleteModelMapping, -} from '@/hooks/queries' -import { Button } from '@/components/ui' -import { ModelInput } from '@/components/ui/model-input' -import { KIRO_COLOR } from '../types' +} from '@/hooks/queries'; +import { Button } from '@/components/ui'; +import { ModelInput } from '@/components/ui/model-input'; +import { KIRO_COLOR } from '../types'; interface KiroProviderViewProps { provider: Provider; @@ -130,25 +125,25 @@ function QuotaCard({ quota }: { quota: KiroQuotaData }) { // Provider Model Mappings Section function ProviderModelMappings({ provider }: { provider: Provider }) { - const { t } = useTranslation() - const { data: allMappings } = useModelMappings() - const createMapping = useCreateModelMapping() - const updateMapping = useUpdateModelMapping() - const deleteMapping = useDeleteModelMapping() - const [newPattern, setNewPattern] = useState('') - const [newTarget, setNewTarget] = useState('') + const { t } = useTranslation(); + const { data: allMappings } = useModelMappings(); + const createMapping = useCreateModelMapping(); + const updateMapping = useUpdateModelMapping(); + const deleteMapping = useDeleteModelMapping(); + const [newPattern, setNewPattern] = useState(''); + const [newTarget, setNewTarget] = useState(''); // Filter mappings for this provider const providerMappings = useMemo(() => { return (allMappings || []).filter( - m => m.scope === 'provider' && m.providerID === provider.id - ) - }, [allMappings, provider.id]) + (m) => m.scope === 'provider' && m.providerID === provider.id, + ); + }, [allMappings, provider.id]); - const isPending = createMapping.isPending || updateMapping.isPending || deleteMapping.isPending + const isPending = createMapping.isPending || updateMapping.isPending || deleteMapping.isPending; const handleAddMapping = async () => { - if (!newPattern.trim() || !newTarget.trim()) return + if (!newPattern.trim() || !newTarget.trim()) return; await createMapping.mutateAsync({ pattern: newPattern.trim(), @@ -158,10 +153,10 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { providerType: 'kiro', priority: providerMappings.length * 10 + 1000, isEnabled: true, - }) - setNewPattern('') - setNewTarget('') - } + }); + setNewPattern(''); + setNewTarget(''); + }; const handleUpdateMapping = async (mapping: ModelMapping, data: Partial) => { await updateMapping.mutateAsync({ @@ -175,29 +170,23 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { priority: mapping.priority, isEnabled: mapping.isEnabled, }, - }) - } + }); + }; const handleDeleteMapping = async (id: number) => { - await deleteMapping.mutateAsync(id) - } + await deleteMapping.mutateAsync(id); + }; return (
-

- {t('modelMappings.title')} -

- - ({providerMappings.length}) - +

{t('modelMappings.title')}

+ ({providerMappings.length})
-

- {t('modelMappings.pageDesc')} -

+

{t('modelMappings.pageDesc')}

{providerMappings.length > 0 && (
@@ -206,7 +195,7 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { {index + 1}. handleUpdateMapping(mapping, { pattern })} + onChange={(pattern) => handleUpdateMapping(mapping, { pattern })} placeholder={t('modelMappings.matchPattern')} disabled={isPending} className="flex-1 min-w-0 h-8 text-sm" @@ -214,7 +203,7 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { handleUpdateMapping(mapping, { target })} + onChange={(target) => handleUpdateMapping(mapping, { target })} placeholder={t('modelMappings.targetModel')} disabled={isPending} className="flex-1 min-w-0 h-8 text-sm" @@ -266,17 +255,13 @@ function ProviderModelMappings({ provider }: { provider: Provider }) {
- ) + ); } -export function KiroProviderView({ - provider, - onDelete, - onClose, -}: KiroProviderViewProps) { - const [quota, setQuota] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) +export function KiroProviderView({ provider, onDelete, onClose }: KiroProviderViewProps) { + const [quota, setQuota] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); const fetchQuota = async () => { setLoading(true); diff --git a/web/src/pages/providers/components/provider-edit-flow.tsx b/web/src/pages/providers/components/provider-edit-flow.tsx index a31da81c..111d4dd3 100644 --- a/web/src/pages/providers/components/provider-edit-flow.tsx +++ b/web/src/pages/providers/components/provider-edit-flow.tsx @@ -1,6 +1,16 @@ -import { useState, useMemo } from 'react' -import { Globe, ChevronLeft, Key, Check, Trash2, Plus, ArrowRight, Zap, Filter } from 'lucide-react' -import { useTranslation } from 'react-i18next' +import { useState, useMemo } from 'react'; +import { + Globe, + ChevronLeft, + Key, + Check, + Trash2, + Plus, + ArrowRight, + Zap, + Filter, +} from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { Dialog, DialogContent, @@ -8,7 +18,7 @@ import { DialogTitle, DialogDescription, DialogFooter, -} from '@/components/ui/dialog' +} from '@/components/ui/dialog'; import { useUpdateProvider, useDeleteProvider, @@ -16,37 +26,43 @@ import { useCreateModelMapping, useUpdateModelMapping, useDeleteModelMapping, -} from '@/hooks/queries' -import type { Provider, ClientType, CreateProviderData, ModelMapping, ModelMappingInput } from '@/lib/transport' -import { defaultClients, type ClientConfig } from '../types' -import { ClientsConfigSection } from './clients-config-section' -import { AntigravityProviderView } from './antigravity-provider-view' -import { KiroProviderView } from './kiro-provider-view' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { ModelInput } from '@/components/ui/model-input' +} from '@/hooks/queries'; +import type { + Provider, + ClientType, + CreateProviderData, + ModelMapping, + ModelMappingInput, +} from '@/lib/transport'; +import { defaultClients, type ClientConfig } from '../types'; +import { ClientsConfigSection } from './clients-config-section'; +import { AntigravityProviderView } from './antigravity-provider-view'; +import { KiroProviderView } from './kiro-provider-view'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { ModelInput } from '@/components/ui/model-input'; // Provider Model Mappings Section for Custom Providers function ProviderModelMappings({ provider }: { provider: Provider }) { - const { t } = useTranslation() - const { data: allMappings } = useModelMappings() - const createMapping = useCreateModelMapping() - const updateMapping = useUpdateModelMapping() - const deleteMapping = useDeleteModelMapping() - const [newPattern, setNewPattern] = useState('') - const [newTarget, setNewTarget] = useState('') + const { t } = useTranslation(); + const { data: allMappings } = useModelMappings(); + const createMapping = useCreateModelMapping(); + const updateMapping = useUpdateModelMapping(); + const deleteMapping = useDeleteModelMapping(); + const [newPattern, setNewPattern] = useState(''); + const [newTarget, setNewTarget] = useState(''); // Filter mappings for this provider const providerMappings = useMemo(() => { return (allMappings || []).filter( - m => m.scope === 'provider' && m.providerID === provider.id - ) - }, [allMappings, provider.id]) + (m) => m.scope === 'provider' && m.providerID === provider.id, + ); + }, [allMappings, provider.id]); - const isPending = createMapping.isPending || updateMapping.isPending || deleteMapping.isPending + const isPending = createMapping.isPending || updateMapping.isPending || deleteMapping.isPending; const handleAddMapping = async () => { - if (!newPattern.trim() || !newTarget.trim()) return + if (!newPattern.trim() || !newTarget.trim()) return; await createMapping.mutateAsync({ pattern: newPattern.trim(), @@ -56,10 +72,10 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { providerType: 'custom', priority: providerMappings.length * 10 + 1000, isEnabled: true, - }) - setNewPattern('') - setNewTarget('') - } + }); + setNewPattern(''); + setNewTarget(''); + }; const handleUpdateMapping = async (mapping: ModelMapping, data: Partial) => { await updateMapping.mutateAsync({ @@ -73,29 +89,23 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { priority: mapping.priority, isEnabled: mapping.isEnabled, }, - }) - } + }); + }; const handleDeleteMapping = async (id: number) => { - await deleteMapping.mutateAsync(id) - } + await deleteMapping.mutateAsync(id); + }; return (
-

- {t('modelMappings.title')} -

- - ({providerMappings.length}) - +

{t('modelMappings.title')}

+ ({providerMappings.length})
-

- {t('modelMappings.pageDesc')} -

+

{t('modelMappings.pageDesc')}

{providerMappings.length > 0 && (
@@ -104,7 +114,7 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { {index + 1}. handleUpdateMapping(mapping, { pattern })} + onChange={(pattern) => handleUpdateMapping(mapping, { pattern })} placeholder={t('modelMappings.matchPattern')} disabled={isPending} className="flex-1 min-w-0 h-8 text-sm" @@ -112,7 +122,7 @@ function ProviderModelMappings({ provider }: { provider: Provider }) { handleUpdateMapping(mapping, { target })} + onChange={(target) => handleUpdateMapping(mapping, { target })} placeholder={t('modelMappings.targetModel')} disabled={isPending} className="flex-1 min-w-0 h-8 text-sm" @@ -164,7 +174,7 @@ function ProviderModelMappings({ provider }: { provider: Provider }) {
- ) + ); } // Provider Supported Models Section @@ -172,24 +182,24 @@ function ProviderSupportModels({ supportModels, onChange, }: { - supportModels: string[] - onChange: (models: string[]) => void + supportModels: string[]; + onChange: (models: string[]) => void; }) { - const { t } = useTranslation() - const [newModel, setNewModel] = useState('') + const { t } = useTranslation(); + const [newModel, setNewModel] = useState(''); const handleAddModel = () => { - if (!newModel.trim()) return - const trimmedModel = newModel.trim() + if (!newModel.trim()) return; + const trimmedModel = newModel.trim(); if (!supportModels.includes(trimmedModel)) { - onChange([...supportModels, trimmedModel]) + onChange([...supportModels, trimmedModel]); } - setNewModel('') - } + setNewModel(''); + }; const handleRemoveModel = (model: string) => { - onChange(supportModels.filter(m => m !== model)) - } + onChange(supportModels.filter((m) => m !== model)); + }; return (
@@ -198,14 +208,15 @@ function ProviderSupportModels({

{t('providers.supportModels.title', 'Supported Models')}

- - ({supportModels.length}) - + ({supportModels.length})

- {t('providers.supportModels.desc', 'Configure which models this provider supports. If empty, all models are supported. Supports wildcards like claude-* or gemini-*.')} + {t( + 'providers.supportModels.desc', + 'Configure which models this provider supports. If empty, all models are supported. Supports wildcards like claude-* or gemini-*.', + )}

{supportModels.length > 0 && ( @@ -231,7 +242,10 @@ function ProviderSupportModels({ {supportModels.length === 0 && (

- {t('providers.supportModels.empty', 'No model filter configured. All models will be supported.')} + {t( + 'providers.supportModels.empty', + 'No model filter configured. All models will be supported.', + )}

)} @@ -243,19 +257,14 @@ function ProviderSupportModels({ placeholder={t('providers.supportModels.placeholder', 'e.g. claude-* or gemini-2.5-*')} className="flex-1 min-w-0 h-8 text-sm" /> -
- ) + ); } interface ProviderEditFlowProps { diff --git a/web/src/pages/providers/components/provider-row.tsx b/web/src/pages/providers/components/provider-row.tsx index e1595f1e..48bbf642 100644 --- a/web/src/pages/providers/components/provider-row.tsx +++ b/web/src/pages/providers/components/provider-row.tsx @@ -186,7 +186,9 @@ export function ProviderRow({ provider, stats, streamingCount, onClick }: Provid {/* 对于 Antigravity,显示 Claude Quota;对于其他类型,显示邮箱/endpoint */} {isAntigravity && claudeInfo ? (
- Claude + + Claude +
{ diff --git a/web/src/pages/requests/index.tsx b/web/src/pages/requests/index.tsx index d1bff602..13bbd4af 100644 --- a/web/src/pages/requests/index.tsx +++ b/web/src/pages/requests/index.tsx @@ -123,7 +123,7 @@ export function RequestsPage() { {/* Content */} -
+
{isLoading && requests.length === 0 ? (
@@ -137,7 +137,7 @@ export function RequestsPage() {

{t('requests.noRequestsHint')}

) : ( -
+
@@ -205,7 +205,7 @@ export function RequestsPage() { {/* Pagination */} -
+
{total > 0 ? t('requests.pageInfo', { @@ -216,21 +216,13 @@ export function RequestsPage() { : t('requests.noItems')}
- {t('requests.page', { current: pageIndex + 1 })} -
diff --git a/web/src/pages/stats/index.tsx b/web/src/pages/stats/index.tsx index 9ca7d374..270eddc1 100644 --- a/web/src/pages/stats/index.tsx +++ b/web/src/pages/stats/index.tsx @@ -17,7 +17,14 @@ import { TabsTrigger, Button, } from '@/components/ui'; -import { useUsageStats, useProviders, useProjects, useAPITokens, useRecalculateUsageStats, useResponseModels } from '@/hooks/queries'; +import { + useUsageStats, + useProviders, + useProjects, + useAPITokens, + useRecalculateUsageStats, + useResponseModels, +} from '@/hooks/queries'; import type { UsageStatsFilter, UsageStats, StatsGranularity } from '@/lib/transport'; import { ComposedChart, @@ -138,7 +145,7 @@ function aggregateForChart( stats: UsageStats[] | undefined, granularity: StatsGranularity, timeRange: TimeRange, - timeConfig: TimeRangeConfig + timeConfig: TimeRangeConfig, ): ChartDataPoint[] { const dataMap = new Map(); @@ -327,7 +334,7 @@ export function StatsPage() { const { data: stats, isLoading } = useUsageStats(filter); const chartData = useMemo( () => aggregateForChart(stats, timeConfig.granularity, timeRange, timeConfig), - [stats, timeConfig, timeRange] + [stats, timeConfig, timeRange], ); const recalculateMutation = useRecalculateUsageStats(); @@ -354,14 +361,23 @@ export function StatsPage() { totalCost: acc.totalCost + s.cost, totalDurationMs: acc.totalDurationMs + s.totalDurationMs, }), - { totalRequests: 0, successfulRequests: 0, failedRequests: 0, totalTokens: 0, totalCost: 0, totalDurationMs: 0 } + { + totalRequests: 0, + successfulRequests: 0, + failedRequests: 0, + totalTokens: 0, + totalCost: 0, + totalDurationMs: 0, + }, ); // 基于 totalDurationMs 计算 RPM 和 TPM // RPM = (totalRequests / totalDurationMs) * 60000 // TPM = (totalTokens / totalDurationMs) * 60000 - const avgRpm = totals.totalDurationMs > 0 ? (totals.totalRequests / totals.totalDurationMs) * 60000 : 0; - const avgTpm = totals.totalDurationMs > 0 ? (totals.totalTokens / totals.totalDurationMs) * 60000 : 0; + const avgRpm = + totals.totalDurationMs > 0 ? (totals.totalRequests / totals.totalDurationMs) * 60000 : 0; + const avgTpm = + totals.totalDurationMs > 0 ? (totals.totalTokens / totals.totalDurationMs) * 60000 : 0; return { ...totals, @@ -384,7 +400,9 @@ export function StatsPage() { onClick={() => recalculateMutation.mutate()} disabled={recalculateMutation.isPending} > - + {t('stats.recalculate')} } @@ -480,7 +498,15 @@ export function StatsPage() { 0 ? ((summary.successfulRequests / summary.totalRequests) * 100).toFixed(1) : 0}%`} - className={summary.totalRequests > 0 && summary.successfulRequests / summary.totalRequests >= 0.95 ? 'text-green-600' : summary.totalRequests > 0 && summary.successfulRequests / summary.totalRequests < 0.8 ? 'text-red-600' : 'text-yellow-600'} + className={ + summary.totalRequests > 0 && + summary.successfulRequests / summary.totalRequests >= 0.95 + ? 'text-green-600' + : summary.totalRequests > 0 && + summary.successfulRequests / summary.totalRequests < 0.8 + ? 'text-red-600' + : 'text-yellow-600' + } /> {t('common.noData')}
) : ( - + {t('stats.chart')} setChartView(v as ChartView)}> @@ -515,30 +541,88 @@ export function StatsPage() { - `$${v.toFixed(2)}`} /> + `$${v.toFixed(2)}`} + /> { const numValue = typeof value === 'number' ? value : 0; const nameStr = name ?? ''; - if (nameStr === t('stats.costUSD')) return [`$${numValue.toFixed(4)}`, nameStr]; + if (nameStr === t('stats.costUSD')) + return [`$${numValue.toFixed(4)}`, nameStr]; return [numValue.toLocaleString(), nameStr]; }} /> {chartView === 'requests' && ( <> - - - + + + )} {chartView === 'tokens' && ( <> - - - - - + + + + + )} @@ -600,7 +684,9 @@ function SummaryCard({
{title}
{value} - {subtitle && {subtitle}} + {subtitle && ( + {subtitle} + )}