import { useCloudAccounts, useRefreshQuota, useDeleteCloudAccount, useAddGoogleAccount, useSwitchCloudAccount, useAutoSwitchEnabled, useSetAutoSwitchEnabled, useForcePollCloudMonitor, useSyncLocalAccount, useOAuthClients, useSetActiveOAuthClient, startAuthFlow, useExportCloudAccounts, useImportCloudAccounts, } from '@/hooks/useCloudAccounts'; import { CloudAccountCard, CompactCloudAccountCard } 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Plus, Cloud, Zap, RefreshCcw, Download, CheckSquare, Trash2, X, RefreshCw, LayoutGrid, List, Columns2, Columns3, SortAsc, LayoutList, Upload, FileDown, Check, Loader2, } 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, useCallback } 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'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { filter, flatMap, isEmpty, isNumber, size, sumBy } from 'lodash-es'; import { clampQuotaPercentage, formatAiCreditsAmount, getQuotaStatus, roundQuotaPercentage, getAccountSortValue, type QuotaStatus, } from '@/utils/quota-display'; import { shouldAutoSubmitGoogleAuthCode } from '@/utils/googleAuthSubmission'; export type GridLayout = 'auto' | '2-col' | '3-col' | 'list' | 'compact'; 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', compact: 'flex flex-col gap-2', }; const GLOBAL_QUOTA_BAR_COLOR_CLASS_BY_STATUS: Record = { high: 'bg-emerald-500', medium: 'bg-amber-500', low: 'bg-rose-500', }; const GLOBAL_QUOTA_TEXT_COLOR_CLASS_BY_STATUS: Record = { high: 'text-emerald-600 dark:text-emerald-400', medium: 'text-amber-600 dark:text-amber-400', low: 'text-rose-600 dark:text-rose-400', }; 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 { data: oauthClients = [], isLoading: isOAuthClientsLoading } = useOAuthClients(); const setActiveOAuthClientMutation = useSetActiveOAuthClient(); const { toast } = useToast(); const lastLoadErrorToastAtRef = useRef(0); const lastSubmittedAuthCodeRef = useRef(null); const gridLayout: GridLayout = (config?.grid_layout as GridLayout) || 'auto'; const updateGridLayout = async (layout: GridLayout) => { if (config) { await saveConfig({ ...config, grid_layout: layout }); } }; const sortOptions = [ 'recently-used', 'quota-overall', 'quota-claude', 'quota-pro3', 'quota-flash', ] as const; type SortOption = (typeof sortOptions)[number]; const currentSort: SortOption = (config?.account_sort as SortOption) || 'recently-used'; const sortI18nKeys: Record = { 'recently-used': 'cloud.sort.recentlyUsed', 'quota-overall': 'cloud.sort.quotaOverall', 'quota-claude': 'cloud.sort.quotaClaude', 'quota-pro3': 'cloud.sort.quotaPro3', 'quota-flash': 'cloud.sort.quotaFlash', }; const sortedAccounts = useMemo(() => { if (!accounts || accounts.length === 0) return []; const activeAccounts = accounts.filter((a) => a.is_active); const otherAccounts = accounts.filter((a) => !a.is_active); const sortedActive = [...activeAccounts].sort((a, b) => { return (b.last_used ?? 0) - (a.last_used ?? 0); }); const sortedOthers = [...otherAccounts].sort((a, b) => { if (currentSort === 'recently-used') { return (b.last_used ?? 0) - (a.last_used ?? 0); } return getAccountSortValue(b, currentSort) - getAccountSortValue(a, currentSort); }); return [...sortedActive, ...sortedOthers]; }, [accounts, currentSort]); // Calculate global quota across all accounts const overallQuotaPercentage = useMemo(() => { if (!accounts || accounts.length === 0) { return null; } const visibilitySettings = config?.model_visibility ?? {}; const visibleModelInfos = flatMap(accounts, (account) => { if (!account.quota?.models) { return []; } return Object.entries(account.quota.models) .filter(([modelName]) => visibilitySettings[modelName] !== false) .map(([, info]) => info); }); if (isEmpty(visibleModelInfos)) { return null; } const averagePercentage = sumBy(visibleModelInfos, (modelInfo) => modelInfo.percentage) / visibleModelInfos.length; return roundQuotaPercentage(averagePercentage); }, [accounts, config?.model_visibility]); const overallQuotaStatus = overallQuotaPercentage === null ? null : getQuotaStatus(overallQuotaPercentage); const effectiveQuotaStatus: QuotaStatus = overallQuotaStatus ?? 'low'; const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [authCode, setAuthCode] = useState(''); const [selectedOAuthClientKey, setSelectedOAuthClientKey] = useState(''); const [identityAccount, setIdentityAccount] = useState(null); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); const [importStrategy, setImportStrategy] = useState<'merge' | 'overwrite' | 'skip-existing'>( 'merge', ); const [importFileContent, setImportFileContent] = useState(null); const [importFileName, setImportFileName] = useState(''); const fileInputRef = useRef(null); const exportMutation = useExportCloudAccounts(); const importMutation = useImportCloudAccounts(); const totalAccounts = size(accounts); const activeAccounts = filter(accounts, (account) => account.is_active).length; const rateLimitedAccounts = filter( accounts, (account) => account.status === 'rate_limited', ).length; const submitAuthCode = useCallback( (incomingAuthCode?: string) => { const codeToUse = incomingAuthCode || authCode; if (!codeToUse) { return; } lastSubmittedAuthCodeRef.current = codeToUse; addMutation.mutate( { authCode: codeToUse, oauthClientKey: selectedOAuthClientKey || oauthClients.find((client) => client.is_active)?.key, }, { onSuccess: () => { setIsAddDialogOpen(false); setAuthCode(''); lastSubmittedAuthCodeRef.current = null; toast({ title: t('cloud.toast.addSuccess') }); }, onError: (err) => { toast({ title: t('cloud.toast.addFailed.title'), description: getLocalizedErrorMessage(err, t), variant: 'destructive', }); }, }, ); }, [addMutation, authCode, oauthClients, selectedOAuthClientKey, t, toast], ); useEffect(() => { if (selectedOAuthClientKey !== '') { return; } const activeClientKey = oauthClients.find((client) => client.is_active)?.key; if (activeClientKey) { setSelectedOAuthClientKey(activeClientKey); } }, [oauthClients, selectedOAuthClientKey]); // Listen for Google Auth Code useEffect(() => { if (window.electron?.onGoogleAuthCode) { console.log('[OAuth] Registering Google auth code IPC listener'); const cleanup = window.electron.onGoogleAuthCode((code) => { console.log('[OAuth] Received Google auth code via IPC:', code?.substring(0, 10) + '...'); lastSubmittedAuthCodeRef.current = null; setAuthCode(code); }); return cleanup; } }, []); // Auto-submit when authCode is set and dialog is open useEffect(() => { if ( shouldAutoSubmitGoogleAuthCode({ authCode, isAddDialogOpen, isPending: addMutation.isPending, lastSubmittedAuthCode: lastSubmittedAuthCodeRef.current, }) ) { console.log('[OAuth] Auto-submitting Google auth code'); submitAuthCode(authCode); } }, [addMutation.isPending, authCode, isAddDialogOpen, submitAuthCode]); // Batch Operations State const [selectedIds, setSelectedIds] = useState>(new Set()); useEffect(() => { if (!isError || !errorUpdatedAt || errorUpdatedAt === lastLoadErrorToastAtRef.current) { return; } toast({ title: t('cloud.error.loadFailed'), description: getLocalizedErrorMessage(error, t), variant: 'destructive', }); lastLoadErrorToastAtRef.current = errorUpdatedAt; }, [error, errorUpdatedAt, isError, t, toast]); const handleRefresh = (id: string) => { console.log(`[Renderer] Triggering refresh for: ${id}`); refreshMutation.mutate( { accountId: id }, { onSuccess: (updatedAccount) => { const credits = updatedAccount.quota?.ai_credits?.credits; if (isNumber(credits)) { toast({ title: t('cloud.toast.quotaRefreshed'), description: t('cloud.toast.refreshCreditsAvailable', { amount: formatAiCreditsAmount(credits), }), }); return; } toast({ title: t('cloud.toast.quotaRefreshed'), description: t('cloud.toast.refreshCreditsUnavailable'), }); }, 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 openGoogleAuthSignIn = async () => { try { lastSubmittedAuthCodeRef.current = null; const effectiveClientKey = selectedOAuthClientKey || oauthClients.find((client) => client.is_active)?.key; await startAuthFlow( effectiveClientKey ? { oauthClientKey: effectiveClientKey, } : undefined, ); } catch (e) { toast({ title: t('cloud.toast.startAuthFailed'), description: String(e), variant: 'destructive', }); } }; const handleExport = async (stripTokens: boolean) => { let url: string | null = null; try { const jsonContent: string = await exportMutation.mutateAsync({ stripTokens }); const blob = new Blob([jsonContent], { type: 'application/json' }); url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `cloud-accounts-export-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); setIsExportDialogOpen(false); toast({ title: t('cloud.exportImport.exportSuccess') }); } catch (error) { toast({ title: t('cloud.error.loadFailed'), description: getLocalizedErrorMessage(error, t), variant: 'destructive', }); } finally { if (url) { URL.revokeObjectURL(url); } } }; const handleImportFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 5 * 1024 * 1024) { toast({ title: t('cloud.error.loadFailed'), description: t('cloud.exportImport.fileTooLarge'), variant: 'destructive', }); return; } setImportFileName(file.name); const reader = new FileReader(); reader.onload = (event) => { try { const content = event.target?.result as string; JSON.parse(content); setImportFileContent(content); } catch { toast({ title: t('cloud.error.loadFailed'), description: t('cloud.exportImport.invalidJson'), variant: 'destructive', }); setImportFileName(''); setImportFileContent(null); } }; reader.onerror = () => { toast({ title: t('cloud.error.loadFailed'), description: t('cloud.exportImport.readFileFailed'), variant: 'destructive', }); }; reader.readAsText(file); if (e.target) { e.target.value = ''; } }; const handleImport = () => { if (!importFileContent) return; importMutation.mutate( { jsonContent: importFileContent, strategy: importStrategy }, { onSuccess: (result) => { setIsImportDialogOpen(false); setImportFileContent(null); setImportFileName(''); setImportStrategy('merge'); toast({ title: t('cloud.exportImport.importSuccess', { imported: result.imported, updated: result.updated, skipped: result.skipped, }), }); if (result.errors.length > 0) { toast({ title: t('cloud.exportImport.importErrors', { count: result.errors.length }), description: result.errors.slice(0, 3).join('\n'), variant: 'destructive', }); } }, onError: (err) => { toast({ title: t('cloud.error.loadFailed'), description: getLocalizedErrorMessage(err, t), variant: 'destructive', }); }, }, ); }; // Batch Selection Handlers const setSelectionState = (id: string, selected: boolean) => { setSelectedIds((prev) => { const next = new Set(prev); if (selected) { next.add(id); } else { next.delete(id); } return next; }); }; const toggleSelectAllAccounts = () => { if (selectedIds.size === accounts?.length) { setSelectedIds(new Set()); } else { setSelectedIds(new Set(accounts?.map((a) => a.id) || [])); } }; const refreshSelectedAccounts = async () => { const ids = Array.from(selectedIds); const results = await Promise.allSettled( ids.map((id) => refreshMutation.mutateAsync({ accountId: id })), ); const successful = results.filter((r) => r.status === 'fulfilled').length; const failed = results.filter((r) => r.status === 'rejected').length; if (failed === 0) { toast({ title: t('cloud.toast.quotaRefreshed'), description: t('cloud.toast.batchRefreshSuccess', { count: successful }), }); } else { toast({ title: t('cloud.toast.batchRefreshPartial.title'), description: t('cloud.toast.batchRefreshPartial.description', { successful, failed, }), variant: 'destructive', }); } setSelectedIds(new Set()); }; const deleteSelectedAccounts = async () => { if (confirm(t('cloud.batch.confirmDelete', { count: selectedIds.size }))) { const ids = Array.from(selectedIds); const results = await Promise.allSettled( ids.map((id) => deleteMutation.mutateAsync({ accountId: id })), ); const successful = results.filter((r) => r.status === 'fulfilled').length; const failed = results.filter((r) => r.status === 'rejected').length; if (failed === 0) { toast({ title: t('cloud.toast.deleted'), description: t('cloud.toast.batchDeleteSuccess', { count: successful }), }); } else { toast({ title: t('cloud.toast.batchDeletePartial.title'), description: t('cloud.toast.batchDeletePartial.description', { successful, failed, }), variant: 'destructive', }); } 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 */} {overallQuotaPercentage !== null && (
{t('cloud.globalQuota')}
{overallQuotaPercentage}%
)}
{t('cloud.exportImport.exportTitle')} {t('cloud.exportImport.exportDesc')} { setIsImportDialogOpen(open); if (!open) { setImportFileContent(null); setImportFileName(''); setImportStrategy('merge'); } }} > {t('cloud.exportImport.importTitle')} {t('cloud.exportImport.importDesc')}
{ setIsAddDialogOpen(open); if (!open) { setAuthCode(''); lastSubmittedAuthCodeRef.current = null; } }} > {t('cloud.authDialog.title')} {t('cloud.authDialog.description')}
) => { setAuthCode(e.target.value); }} />

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

{/* Sort + Layout Selector */}
{sortOptions.map((option) => ( { if (config) { await saveConfig({ ...config, account_sort: option }); } }} > {currentSort === option && } {t(sortI18nKeys[option])} ))}
{t('cloud.layout.auto')} {t('cloud.layout.twoCol')} {t('cloud.layout.threeCol')} {t('cloud.layout.list')} {t('cloud.layout.compact')}
{sortedAccounts.map((account) => gridLayout === 'compact' ? ( ) : ( ), )} {sortedAccounts.length === 0 && (
{t('cloud.list.noAccounts')}
)}
{/* Batch Action Bar */} {selectedIds.size > 0 && (
{t('cloud.batch.selected', { count: selectedIds.size })}
)} { if (!nextOpen) { setIdentityAccount(null); } }} />
); }