diff --git a/web/src/components/cooldown-details-dialog.tsx b/web/src/components/cooldown-details-dialog.tsx index d6bea3ea..aeda00ad 100644 --- a/web/src/components/cooldown-details-dialog.tsx +++ b/web/src/components/cooldown-details-dialog.tsx @@ -212,7 +212,7 @@ export function CooldownDetailsDialog({ {/* Timer Section */}
{/* Countdown */} -
+
diff --git a/web/src/components/force-project-dialog.tsx b/web/src/components/force-project-dialog.tsx index 0c75d841..748f39cc 100644 --- a/web/src/components/force-project-dialog.tsx +++ b/web/src/components/force-project-dialog.tsx @@ -15,9 +15,9 @@ import { cn } from '@/lib/utils'; import { getClientName, getClientColor } from '@/components/icons/client-icons'; interface ForceProjectDialogProps { - event: NewSessionPendingEvent | null; - onClose: () => void; - timeoutSeconds: number; + event: NewSessionPendingEvent | null + onClose: () => void + timeoutSeconds: number } export function ForceProjectDialog({ @@ -25,72 +25,72 @@ export function ForceProjectDialog({ onClose, timeoutSeconds, }: ForceProjectDialogProps) { - const { data: projects, isLoading } = useProjects(); - const updateSessionProject = useUpdateSessionProject(); - const rejectSession = useRejectSession(); - const [selectedProjectId, setSelectedProjectId] = useState(0); - const [remainingTime, setRemainingTime] = useState(timeoutSeconds); + const { data: projects, isLoading } = useProjects() + const updateSessionProject = useUpdateSessionProject() + const rejectSession = useRejectSession() + const [selectedProjectId, setSelectedProjectId] = useState(0) + const [remainingTime, setRemainingTime] = useState(timeoutSeconds) // Reset state when event changes useEffect(() => { if (event) { - setSelectedProjectId(0); - setRemainingTime(timeoutSeconds); + setSelectedProjectId(0) + setRemainingTime(timeoutSeconds) } - }, [event, timeoutSeconds]); + }, [event, timeoutSeconds]) // Countdown timer useEffect(() => { - if (!event) return; + if (!event) return const interval = setInterval(() => { setRemainingTime(prev => { if (prev <= 1) { - clearInterval(interval); - return 0; + clearInterval(interval) + return 0 } - return prev - 1; - }); - }, 1000); + return prev - 1 + }) + }, 1000) - return () => clearInterval(interval); - }, [event]); + return () => clearInterval(interval) + }, [event]) // 超时后关闭弹窗 useEffect(() => { if (remainingTime === 0 && event) { - onClose(); + onClose() } - }, [remainingTime, event, onClose]); + }, [remainingTime, event, onClose]) const handleConfirm = async () => { - if (!event || selectedProjectId === 0) return; + if (!event || selectedProjectId === 0) return try { await updateSessionProject.mutateAsync({ sessionID: event.sessionID, projectID: selectedProjectId, - }); - onClose(); + }) + onClose() } catch (error) { - console.error('Failed to bind project:', error); + console.error('Failed to bind project:', error) } - }; + } const handleReject = async () => { - if (!event) return; + if (!event) return try { - await rejectSession.mutateAsync(event.sessionID); - onClose(); + await rejectSession.mutateAsync(event.sessionID) + onClose() } catch (error) { - console.error('Failed to reject session:', error); + console.error('Failed to reject session:', error) } - }; + } - if (!event) return null; + if (!event) return null - const clientColor = getClientColor(event.clientType); + const clientColor = getClientColor(event.clientType) return ( !open && onClose()}> @@ -124,7 +124,10 @@ export function ForceProjectDialog({ {getClientName(event.clientType)} @@ -140,8 +143,8 @@ export function ForceProjectDialog({ className={cn( 'relative overflow-hidden rounded-xl border p-5 flex flex-col items-center justify-center group', remainingTime <= 10 - ? 'bg-gradient-to-br from-red-950/30 to-transparent border-red-500/20' - : 'bg-gradient-to-br from-amber-950/30 to-transparent border-amber-500/20' + ? 'bg-linear-to-br from-red-950/30 to-transparent border-red-500/20' + : 'bg-linear-to-br from-amber-950/30 to-transparent border-amber-500/20' )} >
{rejectSession.isPending ? ( @@ -235,7 +240,11 @@ export function ForceProjectDialog({ {/* Confirm Button */} +
@@ -235,7 +244,7 @@ export function ProviderDetailsDialog({ className={cn( 'relative w-14 h-14 lg:w-16 lg:h-16 rounded-2xl flex items-center justify-center border shadow-lg', isInCooldown - ? 'bg-cyan-900/40 border-cyan-500/30' + ? 'bg-cyan-500/10 dark:bg-cyan-950/40 border-cyan-500/40 dark:border-cyan-500/30' : 'bg-surface-secondary border-border' )} style={!isInCooldown ? { color } : {}} @@ -244,7 +253,7 @@ export function ProviderDetailsDialog({ className={cn( 'text-2xl lg:text-3xl font-black', isInCooldown - ? 'text-cyan-400 opacity-20 scale-150 blur-[1px]' + ? 'text-cyan-400 dark:text-cyan-300 opacity-20 scale-150 blur-[1px]' : '' )} > @@ -253,7 +262,7 @@ export function ProviderDetailsDialog({ {isInCooldown && ( )}
@@ -265,11 +274,11 @@ export function ProviderDetailsDialog({
{isNative ? ( - + NATIVE ) : ( - + CONVERTED )} @@ -277,7 +286,7 @@ export function ProviderDetailsDialog({ {provider.type} {streamingCount > 0 && ( - + {streamingCount} Streaming )} @@ -291,7 +300,7 @@ export function ProviderDetailsDialog({ {/* 左侧:Provider 信息 + 操作 */}
{/* Provider Basic Info Card */} -
+
@@ -330,44 +339,37 @@ export function ProviderDetailsDialog({
{/* Cooldown Actions (if in cooldown) */} {isInCooldown && ( - + )} {/* Delete Button */} {onDelete && ( - + )} {/* Warning Note */} @@ -384,8 +386,8 @@ export function ProviderDetailsDialog({
{/* Cooldown Warning (if in cooldown) */} {isInCooldown && cooldown && ( -
-
+
+
冷却保护激活
@@ -393,7 +395,7 @@ export function ProviderDetailsDialog({
{/* Reason Section */}
{/* Timer Section */} -
-
-
+
+
+
Remaining
-
+
{liveCountdown}
{(() => { const untilDateStr = formatUntilTime(cooldown.untilTime) return ( -
+
{untilDateStr}
@@ -459,33 +461,38 @@ export function ProviderDetailsDialog({ {stats && stats.totalRequests > 0 ? (
{/* Requests */} -
+
- - + + Requests
- Total - + + Total + + {stats.totalRequests}
- + OK - + {stats.successfulRequests}
- + Fail - + {stats.failedRequests}
@@ -493,22 +500,25 @@ export function ProviderDetailsDialog({
{/* Success Rate */} -
+
- - + + Success Rate
= 95 - ? 'text-emerald-500' + ? 'text-emerald-600 dark:text-emerald-400' : stats.successRate >= 90 - ? 'text-blue-400' - : 'text-amber-500' + ? 'text-blue-600 dark:text-blue-400' + : 'text-amber-600 dark:text-amber-400' )} > {Math.round(stats.successRate)}% @@ -517,29 +527,38 @@ export function ProviderDetailsDialog({
{/* Tokens */} -
+
- - + + Tokens
- In - + + In + + {formatTokens(stats.totalInputTokens)}
- Out - + + Out + + {formatTokens(stats.totalOutputTokens)}
- Cache - + + Cache + + {formatTokens( stats.totalCacheRead + stats.totalCacheWrite )} @@ -549,25 +568,28 @@ export function ProviderDetailsDialog({
{/* Cost */} -
+
- - + + Cost
-
+
{formatCost(stats.totalCost)}
-
+
Cache: {calcCacheRate(stats).toFixed(1)}%
) : ( -
+
No Statistics Available diff --git a/web/src/components/routes/ClientTypeRoutesContent.tsx b/web/src/components/routes/ClientTypeRoutesContent.tsx index 66db6b9f..3699fd74 100644 --- a/web/src/components/routes/ClientTypeRoutesContent.tsx +++ b/web/src/components/routes/ClientTypeRoutesContent.tsx @@ -232,7 +232,7 @@ export function ClientTypeRoutesContent({ items={items} strategy={verticalListSortingStrategy} > -
+
{items.map((item, index) => ( {isNative ? ( - + NATIVE ) : ( diff --git a/web/src/hooks/use-cooldowns.ts b/web/src/hooks/use-cooldowns.ts index ac7a9638..ecb4eff3 100644 --- a/web/src/hooks/use-cooldowns.ts +++ b/web/src/hooks/use-cooldowns.ts @@ -71,7 +71,11 @@ export function useCooldowns() { // Helper to get remaining time as seconds const getRemainingSeconds = (cooldown: Cooldown) => { - const until = new Date(cooldown.untilTime); + // Handle both 'untilTime' and 'until' field names for backward compatibility + const untilTime = cooldown.untilTime || (cooldown as Record).until as string; + if (!untilTime) return 0; + + const until = new Date(untilTime); const now = new Date(); const diff = until.getTime() - now.getTime(); return Math.max(0, Math.floor(diff / 1000)); @@ -81,7 +85,7 @@ export function useCooldowns() { const formatRemaining = (cooldown: Cooldown) => { const seconds = getRemainingSeconds(cooldown); - if (seconds === 0) return 'Expired'; + if (Number.isNaN(seconds) || seconds === 0) return 'Expired'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); diff --git a/web/src/pages/client-routes/components/provider-row.tsx b/web/src/pages/client-routes/components/provider-row.tsx index 1003122a..732b4315 100644 --- a/web/src/pages/client-routes/components/provider-row.tsx +++ b/web/src/pages/client-routes/components/provider-row.tsx @@ -6,7 +6,7 @@ import { Snowflake, Info, } from 'lucide-react' -import { Switch } from '@/components/ui' +import { Button, Switch } from '@/components/ui' import { StreamingBadge } from '@/components/ui/streaming-badge' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' @@ -250,15 +250,16 @@ export function ProviderRowContent({ } return ( -
0 - ? 'bg-surface-primary border-transparent ring-1 ring-black/5 dark:ring-white/10' + ? 'bg-accent/5 border-transparent ring-1 ring-black/5 dark:ring-white/10' : 'bg-surface-primary/60 border-border hover:border-emerald-500/30 hover:bg-surface-primary shadow-sm cursor-pointer' : 'bg-surface-secondary/40 border-dashed border-border opacity-70 cursor-pointer grayscale-[0.5] hover:opacity-100 hover:grayscale-0' )} @@ -284,8 +285,8 @@ export function ProviderRowContent({ {/* Cooldown 冰冻效果 - 增强版 */} {isInCooldown && ( <> -
-
+
+
{/* 雪花动画 (CSS Background) */}
@@ -350,7 +351,7 @@ export function ProviderRowContent({ className={cn( 'text-[14px] font-bold truncate transition-colors', isInCooldown - ? 'text-cyan-200' + ? 'text-text-primary' : enabled ? 'text-text-primary' : 'text-text-muted' @@ -375,7 +376,7 @@ export function ProviderRowContent({ className={cn( 'text-[11px] font-medium truncate flex items-center gap-1', isInCooldown - ? 'text-cyan-500/70' + ? 'text-text-muted' : enabled ? 'text-text-muted' : 'text-text-muted/50' @@ -435,23 +436,23 @@ export function ProviderRowContent({ {/* Center-placed Countdown (when in cooldown) or Stats Grid */} {isInCooldown && cooldown ? ( -
+
- + Remaining
- + {liveCountdown}
-
-
+
+
FROZEN
@@ -517,10 +518,15 @@ export function ProviderRowContent({
)}
- + {/* Streaming Indicator - Inline before Switch */} + {enabled && streamingCount > 0 && !isInCooldown && ( +
+ +
+ )} {/* Control Area - Switch */}
e.stopPropagation()} onPointerDown={e => e.stopPropagation()} > @@ -530,13 +536,6 @@ export function ProviderRowContent({ disabled={isToggling} />
- - {/* Streaming Indicator - Top Right */} - {enabled && streamingCount > 0 && !isInCooldown && ( -
- -
- )} -
+ ) } diff --git a/web/src/pages/overview.tsx b/web/src/pages/overview.tsx index d9487157..5a9d332b 100644 --- a/web/src/pages/overview.tsx +++ b/web/src/pages/overview.tsx @@ -85,7 +85,7 @@ export function OverviewPage() { {/* Welcome Section */} {!hasProviders && (
-
+

diff --git a/web/src/pages/providers/components/provider-create-flow.tsx b/web/src/pages/providers/components/provider-create-flow.tsx index 135dc4fd..3401cfe8 100644 --- a/web/src/pages/providers/components/provider-create-flow.tsx +++ b/web/src/pages/providers/components/provider-create-flow.tsx @@ -1,23 +1,31 @@ -import { useState } from 'react'; -import { Globe, ChevronLeft, Key, Check } from 'lucide-react'; -import { useCreateProvider } from '@/hooks/queries'; -import type { ClientType, CreateProviderData } from '@/lib/transport'; -import { quickTemplates, defaultClients, type ClientConfig, type ProviderFormData, type CreateStep } from '../types'; -import { ClientsConfigSection } from './clients-config-section'; -import { SelectTypeStep } from './select-type-step'; -import { AntigravityTokenImport } from './antigravity-token-import'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; +import { useState } from 'react' +import { Globe, ChevronLeft, Key, Check } from 'lucide-react' +import { useCreateProvider } from '@/hooks/queries' +import type { ClientType, CreateProviderData } from '@/lib/transport' +import { + quickTemplates, + defaultClients, + type ClientConfig, + type ProviderFormData, + type CreateStep, +} from '../types' +import { ClientsConfigSection } from './clients-config-section' +import { SelectTypeStep } from './select-type-step' +import { AntigravityTokenImport } from './antigravity-token-import' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' interface ProviderCreateFlowProps { - onClose: () => void; + onClose: () => void } export function ProviderCreateFlow({ onClose }: ProviderCreateFlowProps) { - const [step, setStep] = useState('select-type'); - const [saving, setSaving] = useState(false); - const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle'); - const createProvider = useCreateProvider(); + const [step, setStep] = useState('select-type') + const [saving, setSaving] = useState(false) + const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>( + 'idle' + ) + const createProvider = useCreateProvider() const [formData, setFormData] = useState({ type: 'custom', @@ -26,64 +34,73 @@ export function ProviderCreateFlow({ onClose }: ProviderCreateFlowProps) { baseURL: '', apiKey: '', clients: [...defaultClients], - }); + }) const selectType = (type: 'custom' | 'antigravity') => { - setFormData((prev) => ({ ...prev, type })); + setFormData(prev => ({ ...prev, type })) if (type === 'antigravity') { - setStep('antigravity-import'); + setStep('antigravity-import') } - }; + } const applyTemplate = (templateId: string) => { - const template = quickTemplates.find((t) => t.id === templateId); + const template = quickTemplates.find(t => t.id === templateId) if (template) { - const updatedClients = defaultClients.map((client) => { - const isSupported = template.supportedClients.includes(client.id); - const baseURL = template.clientBaseURLs[client.id] || ''; - return { ...client, enabled: isSupported, urlOverride: baseURL }; - }); + const updatedClients = defaultClients.map(client => { + const isSupported = template.supportedClients.includes(client.id) + const baseURL = template.clientBaseURLs[client.id] || '' + return { ...client, enabled: isSupported, urlOverride: baseURL } + }) - setFormData((prev) => ({ + setFormData(prev => ({ ...prev, selectedTemplate: templateId, name: template.name, clients: updatedClients, - })); + })) - setStep('custom-config'); + setStep('custom-config') } - }; + } - const updateClient = (clientId: ClientType, updates: Partial) => { - setFormData((prev) => ({ + const updateClient = ( + clientId: ClientType, + updates: Partial + ) => { + setFormData(prev => ({ ...prev, - clients: prev.clients.map((c) => (c.id === clientId ? { ...c, ...updates } : c)), - })); - }; + clients: prev.clients.map(c => + c.id === clientId ? { ...c, ...updates } : c + ), + })) + } const isValid = () => { - if (!formData.name.trim()) return false; - if (!formData.apiKey.trim()) return false; - const hasEnabledClient = formData.clients.some((c) => c.enabled); - const hasUrl = formData.baseURL.trim() || formData.clients.some((c) => c.enabled && c.urlOverride.trim()); - return hasEnabledClient && hasUrl; - }; + if (!formData.name.trim()) return false + if (!formData.apiKey.trim()) return false + const hasEnabledClient = formData.clients.some(c => c.enabled) + const hasUrl = + formData.baseURL.trim() || + formData.clients.some(c => c.enabled && c.urlOverride.trim()) + return hasEnabledClient && hasUrl + } const handleSave = async () => { - if (!isValid()) return; + if (!isValid()) return - setSaving(true); - setSaveStatus('idle'); + setSaving(true) + setSaveStatus('idle') try { - const supportedClientTypes = formData.clients.filter((c) => c.enabled).map((c) => c.id); - const clientBaseURL: Partial> = {}; - formData.clients.forEach((c) => { + const supportedClientTypes = formData.clients + .filter(c => c.enabled) + .map(c => c.id) + const clientBaseURL: Partial> = {} + formData.clients.forEach(c => { if (c.enabled && c.urlOverride) { - clientBaseURL[c.id] = c.urlOverride; + clientBaseURL[c.id] = c.urlOverride } - }); + }) const data: CreateProviderData = { type: 'custom', @@ -92,30 +109,31 @@ export function ProviderCreateFlow({ onClose }: ProviderCreateFlowProps) { custom: { baseURL: formData.baseURL, apiKey: formData.apiKey, - clientBaseURL: Object.keys(clientBaseURL).length > 0 ? clientBaseURL : undefined, + clientBaseURL: + Object.keys(clientBaseURL).length > 0 ? clientBaseURL : undefined, }, }, supportedClientTypes, - }; + } - await createProvider.mutateAsync(data); - setSaveStatus('success'); - setTimeout(() => onClose(), 500); + await createProvider.mutateAsync(data) + setSaveStatus('success') + setTimeout(() => onClose(), 500) } catch (error) { - console.error('Failed to create provider:', error); - setSaveStatus('error'); + console.error('Failed to create provider:', error) + setSaveStatus('error') } finally { - setSaving(false); + setSaving(false) } - }; + } const handleBack = () => { if (step === 'custom-config' || step === 'antigravity-import') { - setStep('select-type'); + setStep('select-type') } else { - onClose(); + onClose() } - }; + } if (step === 'select-type') { return ( @@ -126,31 +144,39 @@ export function ProviderCreateFlow({ onClose }: ProviderCreateFlowProps) { onSkipToConfig={() => setStep('custom-config')} onBack={handleBack} /> - ); + ) } if (step === 'antigravity-import') { - const handleCreateAntigravityProvider = async (data: CreateProviderData) => { - await createProvider.mutateAsync(data); - onClose(); - }; - return ; + const handleCreateAntigravityProvider = async ( + data: CreateProviderData + ) => { + await createProvider.mutateAsync(data) + onClose() + } + return ( + + ) } // Custom: Configuration return (
-
+
- +
-

Configure Provider

-

Set up your custom provider connection

+

+ Configure Provider +

+

+ Set up your custom provider connection +

@@ -177,19 +203,22 @@ export function ProviderCreateFlow({ onClose }: ProviderCreateFlowProps) {
-

1. Basic Information

- +
- + setFormData((prev) => ({ ...prev, name: e.target.value }))} + onChange={e => + setFormData(prev => ({ ...prev, name: e.target.value })) + } placeholder="e.g. Production OpenAI" className="w-full" /> @@ -206,7 +235,12 @@ export function ProviderCreateFlow({ onClose }: ProviderCreateFlowProps) { setFormData((prev) => ({ ...prev, baseURL: e.target.value }))} + onChange={e => + setFormData(prev => ({ + ...prev, + baseURL: e.target.value, + })) + } placeholder="https://api.openai.com/v1" className="w-full" /> @@ -225,7 +259,9 @@ export function ProviderCreateFlow({ onClose }: ProviderCreateFlowProps) { setFormData((prev) => ({ ...prev, apiKey: e.target.value }))} + onChange={e => + setFormData(prev => ({ ...prev, apiKey: e.target.value })) + } placeholder="sk-..." className="w-full" /> @@ -235,20 +271,24 @@ export function ProviderCreateFlow({ onClose }: ProviderCreateFlowProps) {
-

- 2. Client Configuration -

- +

+ 2. Client Configuration +

+
{saveStatus === 'error' && (
- Failed to create provider. Please check your connection and try again. + Failed to create provider. Please check your connection and try + again.
)}
- ); + ) } diff --git a/web/src/pages/providers/components/select-type-step.tsx b/web/src/pages/providers/components/select-type-step.tsx index 287d8201..94371506 100644 --- a/web/src/pages/providers/components/select-type-step.tsx +++ b/web/src/pages/providers/components/select-type-step.tsx @@ -1,12 +1,24 @@ -import { Server, Wand2, ChevronLeft, Layers, Grid3X3, CheckCircle2, FilePlus } from 'lucide-react'; -import { ANTIGRAVITY_COLOR, quickTemplates, type ProviderFormData } from '../types'; +import { + Server, + Wand2, + ChevronLeft, + Layers, + Grid3X3, + CheckCircle2, + FilePlus, +} from 'lucide-react' +import { + quickTemplates, + type ProviderFormData, +} from '../types' +import { Button } from '@/components/ui' interface SelectTypeStepProps { - formData: ProviderFormData; - onSelectType: (type: 'custom' | 'antigravity') => void; - onApplyTemplate: (templateId: string) => void; - onSkipToConfig: () => void; - onBack: () => void; + formData: ProviderFormData + onSelectType: (type: 'custom' | 'antigravity') => void + onApplyTemplate: (templateId: string) => void + onSkipToConfig: () => void + onBack: () => void } export function SelectTypeStep({ @@ -18,80 +30,85 @@ export function SelectTypeStep({ }: SelectTypeStepProps) { return (
-
- +
-

Add Provider

-

Choose a service provider to get started

+

+ Add Provider +

+

+ Choose a service provider to get started +

-
+
- {/* Section: Service Provider */}
-

+

1. Choose Service Provider

-
- + - +
@@ -99,69 +116,89 @@ export function SelectTypeStep({ {formData.type === 'custom' && (
-

- 2. Select a Template (Optional) +

+ 2. Select a Template{' '} + + (Optional) +

- -
+ +
{/* Empty Template Card */} - + - {quickTemplates.map((template) => { - const Icon = template.icon === 'grid' ? Grid3X3 : Layers; - const isSelected = formData.selectedTemplate === template.id; + {quickTemplates.map(template => { + const Icon = template.icon === 'grid' ? Grid3X3 : Layers + const isSelected = formData.selectedTemplate === template.id return ( - - ); + + ) })}
@@ -169,5 +206,5 @@ export function SelectTypeStep({
- ); + ) }