import { useCloudAccounts, useRefreshQuota, useDeleteCloudAccount, useAddGoogleAccount, useSwitchCloudAccount, useAutoSwitchEnabled, useSetAutoSwitchEnabled, useForcePollCloudMonitor, useSyncLocalAccount, startAuthFlow, } from '@/hooks/useCloudAccounts'; import { CloudAccountCard } from '@/components/CloudAccountCard'; import { IdentityProfileDialog } from '@/components/IdentityProfileDialog'; import { CloudAccount } from '@/types/cloudAccount'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { Plus, Loader2, Cloud, Zap, RefreshCcw, Download, CheckSquare, Trash2, X, RefreshCw, LayoutGrid, List, Columns2, Columns3, } from 'lucide-react'; import { useToast } from '@/components/ui/use-toast'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useState, useEffect, useRef, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getLocalizedErrorMessage } from '@/utils/errorMessages'; import { useAppConfig } from '@/hooks/useAppConfig'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; // ... (existing code: imports and comments) export type GridLayout = 'auto' | '2-col' | '3-col' | 'list'; const GRID_LAYOUT_CLASSES: Record = { auto: 'grid gap-4 md:grid-cols-2 xl:grid-cols-3', '2-col': 'grid gap-4 grid-cols-2', '3-col': 'grid gap-4 grid-cols-3', list: 'grid gap-4 grid-cols-1', }; export function CloudAccountList() { const { t } = useTranslation(); const { data: accounts, isLoading, isError, error, errorUpdatedAt, refetch } = useCloudAccounts(); const { config, saveConfig } = useAppConfig(); const refreshMutation = useRefreshQuota(); const deleteMutation = useDeleteCloudAccount(); const addMutation = useAddGoogleAccount(); const switchMutation = useSwitchCloudAccount(); const syncMutation = useSyncLocalAccount(); const { data: autoSwitchEnabled, isLoading: isSettingsLoading } = useAutoSwitchEnabled(); const setAutoSwitchMutation = useSetAutoSwitchEnabled(); const forcePollMutation = useForcePollCloudMonitor(); const { toast } = useToast(); const lastCloudLoadErrorToastAt = useRef(0); const gridLayout: GridLayout = (config?.grid_layout as GridLayout) || 'auto'; const setGridLayout = async (layout: GridLayout) => { if (config) { await saveConfig({ ...config, grid_layout: layout }); } }; // Calculate global quota across all accounts const globalQuota = useMemo(() => { if (!accounts || accounts.length === 0) { return null; } const visibilitySettings = config?.model_visibility ?? {}; let totalPercentage = 0; let modelCount = 0; accounts.forEach((account) => { if (!account.quota?.models) { return; } Object.entries(account.quota.models).forEach(([modelName, info]) => { if (visibilitySettings[modelName] !== false) { totalPercentage += info.percentage; modelCount++; } }); }); if (modelCount === 0) { return null; } return Math.round((totalPercentage / modelCount) * 10) / 10; }, [accounts, config?.model_visibility]); const getGlobalQuotaColor = (percentage: number) => { if (percentage > 80) { return 'bg-emerald-500'; } if (percentage > 20) { return 'bg-amber-500'; } return 'bg-rose-500'; }; const getGlobalQuotaTextColor = (percentage: number) => { if (percentage > 80) { return 'text-emerald-600 dark:text-emerald-400'; } if (percentage > 20) { return 'text-amber-600 dark:text-amber-400'; } return 'text-rose-600 dark:text-rose-400'; }; const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [authCode, setAuthCode] = useState(''); const [identityAccount, setIdentityAccount] = useState(null); const totalAccounts = accounts?.length || 0; const activeAccounts = accounts?.filter((account) => account.is_active).length || 0; const rateLimitedAccounts = accounts?.filter((account) => account.status === 'rate_limited').length || 0; const handleAddAccount = (codeVal?: string) => { const codeToUse = codeVal || authCode; if (!codeToUse) { return; } addMutation.mutate( { authCode: codeToUse }, { onSuccess: () => { setIsAddDialogOpen(false); setAuthCode(''); toast({ title: t('cloud.toast.addSuccess') }); }, onError: (err) => { toast({ title: t('cloud.toast.addFailed.title'), description: getLocalizedErrorMessage(err, t), variant: 'destructive', }); }, }, ); }; // Listen for Google Auth Code useEffect(() => { if (window.electron?.onGoogleAuthCode) { console.log('[OAuth] Setting up auth code listener, dialog open:', isAddDialogOpen); const cleanup = window.electron.onGoogleAuthCode((code) => { console.log('[OAuth] Received auth code via IPC:', code?.substring(0, 10) + '...'); setAuthCode(code); // Note: Auto-submit will be triggered by the authCode change effect below }); return cleanup; } }, []); // Auto-submit when authCode is set and dialog is open useEffect(() => { if (authCode && isAddDialogOpen && !addMutation.isPending) { console.log('[OAuth] Auto-submitting auth code'); handleAddAccount(authCode); } }, [authCode, isAddDialogOpen]); // Batch Operations State const [selectedIds, setSelectedIds] = useState>(new Set()); useEffect(() => { if (!isError || !errorUpdatedAt || errorUpdatedAt === lastCloudLoadErrorToastAt.current) { return; } toast({ title: t('cloud.error.loadFailed'), description: getLocalizedErrorMessage(error, t), variant: 'destructive', }); lastCloudLoadErrorToastAt.current = errorUpdatedAt; }, [error, errorUpdatedAt, isError, t, toast]); // ... (existing code: handleRefresh, handleSwitch, handleDelete) const handleRefresh = (id: string) => { refreshMutation.mutate( { accountId: id }, { onSuccess: () => toast({ title: t('cloud.toast.quotaRefreshed') }), onError: () => toast({ title: t('cloud.toast.refreshFailed'), variant: 'destructive' }), }, ); }; const handleSwitch = (id: string) => { switchMutation.mutate( { accountId: id }, { onSuccess: () => toast({ title: t('cloud.toast.switched.title'), description: t('cloud.toast.switched.description'), }), onError: (err) => toast({ title: t('cloud.toast.switchFailed'), description: getLocalizedErrorMessage(err, t), variant: 'destructive', }), }, ); }; const handleDelete = (id: string) => { if (confirm(t('cloud.toast.deleteConfirm'))) { deleteMutation.mutate( { accountId: id }, { onSuccess: () => { toast({ title: t('cloud.toast.deleted') }); // Clear from selection if deleted setSelectedIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); }, onError: () => toast({ title: t('cloud.toast.deleteFailed'), variant: 'destructive' }), }, ); } }; const handleManageIdentity = (id: string) => { const target = (accounts || []).find((item) => item.id === id) || null; setIdentityAccount(target); }; const handleToggleAutoSwitch = (checked: boolean) => { setAutoSwitchMutation.mutate( { enabled: checked }, { onSuccess: () => toast({ title: checked ? t('cloud.toast.autoSwitchOn') : t('cloud.toast.autoSwitchOff'), }), onError: () => toast({ title: t('cloud.toast.updateSettingsFailed'), variant: 'destructive' }), }, ); }; const handleForcePoll = () => { if (forcePollMutation.isPending) return; forcePollMutation.mutate(undefined, { onSuccess: () => toast({ title: t('cloud.polling') }), onError: (err) => toast({ title: t('cloud.toast.pollFailed'), description: getLocalizedErrorMessage(err, t), variant: 'destructive', }), }); }; const handleSyncLocal = () => { syncMutation.mutate(undefined, { onSuccess: (acc: CloudAccount | null) => { if (acc) { toast({ title: t('cloud.toast.syncSuccess.title'), description: t('cloud.toast.syncSuccess.description', { email: acc.email }), }); } else { toast({ title: t('cloud.toast.syncFailed.title'), description: t('cloud.toast.syncFailed.description'), variant: 'destructive', }); } }, onError: (err) => { toast({ title: t('cloud.toast.syncFailed.title'), description: getLocalizedErrorMessage(err, t), variant: 'destructive', }); }, }); }; const openAuthUrl = async () => { try { await startAuthFlow(); } catch (e) { toast({ title: t('cloud.toast.startAuthFailed'), // Need to add this key or just use generic error description: String(e), variant: 'destructive', }); } }; // Batch Selection Handlers const toggleSelection = (id: string, selected: boolean) => { setSelectedIds((prev) => { const next = new Set(prev); if (selected) { next.add(id); } else { next.delete(id); } return next; }); }; const toggleSelectAll = () => { if (selectedIds.size === accounts?.length) { setSelectedIds(new Set()); } else { setSelectedIds(new Set(accounts?.map((a) => a.id) || [])); } }; const handleBatchRefresh = () => { selectedIds.forEach((id) => { refreshMutation.mutate({ accountId: id }); }); toast({ title: t('cloud.toast.quotaRefreshed'), description: `triggered for ${selectedIds.size} accounts.`, }); setSelectedIds(new Set()); }; const handleBatchDelete = () => { if (confirm(t('cloud.batch.confirmDelete', { count: selectedIds.size }))) { selectedIds.forEach((id) => { deleteMutation.mutate({ accountId: id }); }); toast({ title: t('cloud.toast.deleted'), description: `${selectedIds.size} accounts deleted.`, }); setSelectedIds(new Set()); } }; if (isLoading) { return (
); } if (isError) { return (
{t('cloud.error.loadFailed')}
{t('action.retry')}
); } return (

{t('cloud.title')}

{t('cloud.description')}

{t('cloud.card.actions')}
{totalAccounts}
{t('cloud.card.active')}
{activeAccounts}
{t('cloud.card.rateLimited')}
{rateLimitedAccounts}
{/* Global Quota */} {globalQuota !== null && (
{t('cloud.globalQuota')}
{globalQuota}%
)}
{t('cloud.authDialog.title')} {t('cloud.authDialog.description')}
) => setAuthCode(e.target.value)} />

{t('cloud.authDialog.instruction')}

{/* Layout Selector */}
{t('cloud.layout.auto')} {t('cloud.layout.twoCol')} {t('cloud.layout.threeCol')} {t('cloud.layout.list')}
{accounts?.map((account) => ( ))} {accounts?.length === 0 && (
{t('cloud.list.noAccounts')}
)}
{/* Batch Action Bar */} {selectedIds.size > 0 && (
{t('cloud.batch.selected', { count: selectedIds.size })}
)} { if (!nextOpen) { setIdentityAccount(null); } }} />
); }