diff --git a/client/components/AIAccounts.tsx b/client/components/AIAccounts.tsx new file mode 100644 index 0000000..004346a --- /dev/null +++ b/client/components/AIAccounts.tsx @@ -0,0 +1,317 @@ +import { useState, useEffect } from 'react'; +import { UsageProgressBar } from './UsageProgressBar'; + +interface ClaudeAccount { + id: string; + name: string; + status: string; + accountType: string; + lastUsedAt: string | null; + usage?: { + daily?: { + tokens: number; + requests: number; + cost: number; + }; + }; + claudeUsage?: { + fiveHour?: { + utilization: number; + resetsAt: string; + remainingSeconds: number; + }; + sevenDay?: { + utilization: number; + resetsAt: string; + remainingSeconds: number; + }; + sevenDayOpus?: { + utilization: number; + resetsAt: string; + remainingSeconds: number; + }; + }; +} + +interface OpenAIAccount { + id: string; + name: string; + status: string; + accountType: string; + lastUsedAt: string | null; + usage?: { + daily?: { + tokens: number; + requests: number; + cost: number; + }; + }; + codexUsage?: { + primary?: { + usedPercent: number; + resetAfterSeconds: number; + resetAt: string; + }; + secondary?: { + usedPercent: number; + resetAfterSeconds: number; + resetAt: string; + }; + }; +} + +type UnifiedAccount = (ClaudeAccount | OpenAIAccount) & { + platform: 'claude' | 'openai'; +}; + +interface AIAccountsProps { + apiKey: string; +} + +export function AIAccounts({ apiKey }: AIAccountsProps) { + const [claudeAccounts, setClaudeAccounts] = useState([]); + const [openaiAccounts, setOpenaiAccounts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + fetchAccounts(); + const interval = setInterval(fetchAccounts, 30000); + return () => clearInterval(interval); + }, [apiKey]); + + const fetchAccounts = async () => { + try { + const response = await fetch('/api/ai-accounts', { + headers: { + 'X-API-Key': apiKey, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch AI accounts'); + } + + const data = await response.json(); + setClaudeAccounts(data.claude || []); + setOpenaiAccounts(data.openai || []); + setError(''); + } catch (err) { + console.error('Error fetching AI accounts:', err); + setError(err instanceof Error ? err.message : 'Failed to load AI accounts'); + } finally { + setIsLoading(false); + } + }; + + const formatLastUsed = (lastUsedAt: string | null) => { + if (!lastUsedAt) return 'Never'; + const date = new Date(lastUsedAt); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + const diffHours = Math.floor(diffMins / 60); + if (diffHours < 24) return `${diffHours}h ago`; + const diffDays = Math.floor(diffHours / 24); + return `${diffDays}d ago`; + }; + + const formatTime = (seconds: number) => { + if (seconds <= 0) return '0m'; + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (days > 0) { + if (hours > 0) return `${days}d ${hours}h`; + return `${days}d`; + } + if (hours > 0) return `${hours}h ${minutes}m`; + return `${minutes}m`; + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'active': + return 'bg-chart-1 text-white'; + case 'blocked': + return 'bg-chart-2 text-white'; + case 'unauthorized': + return 'bg-chart-3 text-white'; + case 'temp_error': + return 'bg-chart-4 text-white'; + default: + return 'bg-chart-5 text-white'; + } + }; + + const getAccountName = (account: UnifiedAccount, totalCount: number) => { + if (totalCount === 1) { + return account.platform === 'claude' ? 'Claude' : 'OpenAI'; + } + return `${account.platform === 'claude' ? 'Claude' : 'OpenAI'} (${account.name})`; + }; + + // Merge accounts + const allAccounts: UnifiedAccount[] = [ + ...claudeAccounts.map(acc => ({ ...acc, platform: 'claude' as const })), + ...openaiAccounts.map(acc => ({ ...acc, platform: 'openai' as const })), + ]; + + if (isLoading) { + return ( +
+
+

AI Accounts Status

+
+
+ + + + + Loading accounts... +
+
+ ); + } + + if (error) { + return ( +
+
+

AI Accounts Status

+
+
+

{error}

+ +
+
+ ); + } + + return ( +
+
+

AI Accounts Status

+
+ + {allAccounts.length > 0 ? ( +
+ + + + + + + + + + + + {allAccounts.map((account) => { + const platformCount = account.platform === 'claude' ? claudeAccounts.length : openaiAccounts.length; + const isClaudeAccount = account.platform === 'claude'; + const claudeAcc = isClaudeAccount ? (account as ClaudeAccount) : null; + const openaiAcc = !isClaudeAccount ? (account as OpenAIAccount) : null; + + return ( + + + + + + + + ); + })} + +
AccountStatusDaily UsageUsage WindowsLast Used
+
+ {getAccountName(account, platformCount)} +
+
+ + {account.status} + + + {account.usage?.daily && (isClaudeAccount || account.usage.daily.requests > 0) ? ( +
+
{account.usage.daily.requests} reqs
+
${account.usage.daily.cost.toFixed(2)} cost
+
+ ) : ( + {isClaudeAccount ? 'No data' : 'No usage today'} + )} +
+ {claudeAcc?.claudeUsage ? ( +
+ {claudeAcc.claudeUsage.fiveHour && ( + + )} + {claudeAcc.claudeUsage.sevenDay && ( + + )} + {claudeAcc.claudeUsage.sevenDayOpus && ( + + )} +
+ ) : openaiAcc?.codexUsage ? ( +
+ {openaiAcc.codexUsage.primary && ( + + )} + {openaiAcc.codexUsage.secondary && ( + + )} +
+ ) : ( + No data + )} +
{formatLastUsed(account.lastUsedAt)}
+
+ ) : ( +
+ No shared AI accounts found +
+ )} +
+ ); +} diff --git a/client/components/CurrentPeriod.tsx b/client/components/CurrentPeriod.tsx index 1c037ca..53e64a8 100644 --- a/client/components/CurrentPeriod.tsx +++ b/client/components/CurrentPeriod.tsx @@ -193,7 +193,7 @@ export function CurrentPeriod({ period, apiKey, userId }: CurrentPeriodProps) {

Failed to Load Period Data

{error}