import { useEffect, useState, useRef } from 'react'; import { Maximize2, RefreshCw, Clock, ShieldAlert, Tag, Activity } from 'lucide-react'; import { useViewStore } from '../../stores/useViewStore'; import { useAccountStore } from '../../stores/useAccountStore'; import { isTauri } from '../../utils/env'; import { getCurrentWindow } from '@tauri-apps/api/window'; import { motion, AnimatePresence } from 'framer-motion'; import { useTranslation } from 'react-i18next'; import clsx from 'clsx'; import { formatTimeRemaining, formatCompactNumber } from '../../utils/format'; import { enterMiniMode, exitMiniMode } from '../../utils/windowManager'; import { getVersion } from '@tauri-apps/api/app'; import { listen } from '@tauri-apps/api/event'; import { useConfigStore } from '../../stores/useConfigStore'; interface ProxyRequestLog { id: string; model?: string; input_tokens?: number; output_tokens?: number; timestamp: number; status: number; duration: number; mapped_model?: string } export default function MiniView() { const { setMiniView } = useViewStore(); const { currentAccount, refreshQuota, fetchCurrentAccount } = useAccountStore(); const { config } = useConfigStore(); const { t } = useTranslation(); const [isRefreshing, setIsRefreshing] = useState(false); const containerRef = useRef(null); const [appVersion, setAppVersion] = useState('0.0.0'); const [latestLog, setLatestLog] = useState(null); // Subscribe to proxy logs useEffect(() => { let unlistenFn: (() => void) | null = null; const setupListener = async () => { if (!isTauri()) return; try { unlistenFn = await listen('proxy://request', (event) => { console.log(event) setLatestLog(event.payload); }); } catch (e) { console.error('Failed to setup log listener:', e); } }; setupListener(); return () => { if (unlistenFn) unlistenFn(); }; }, []); // Get app version useEffect(() => { const fetchVersion = async () => { if (isTauri()) { try { const version = await getVersion(); setAppVersion(version); } catch (e) { console.error('Failed to get app version:', e); } } else { // Fallback for web mode if needed, or import from package.json setAppVersion('4.1.32'); } }; fetchVersion(); }, []); // Auto-refresh logic based on config useEffect(() => { if (!config?.auto_refresh || !config?.refresh_interval || config.refresh_interval <= 0) return; console.log(`[MiniView] Starting auto-refresh timer: ${config.refresh_interval} mins`); const intervalId = setInterval(() => { if (!isRefreshing && currentAccount) { console.log('[MiniView] Auto-refreshing quota...'); handleRefresh(); } }, config.refresh_interval * 60 * 1000); return () => clearInterval(intervalId); }, [config?.auto_refresh, config?.refresh_interval, currentAccount, isRefreshing]); // Enter mini mode & Auto-resize based on content useEffect(() => { const adjustSize = async () => { if (isTauri() && containerRef.current) { // Get the content height const height = containerRef.current.scrollHeight; // Calculate content height for the utility (which adds 20px padding) // We want final height to be approx (scroll height - header adjustment) await enterMiniMode(height); } }; // Run initially and whenever account data (content) changes // Use a small timeout to ensure rendering is complete const timer = setTimeout(adjustSize, 50); return () => clearTimeout(timer); }, [currentAccount]); const handleRefresh = async () => { if (!currentAccount || isRefreshing) return; setIsRefreshing(true); try { await refreshQuota(currentAccount.id); await fetchCurrentAccount(); } finally { setTimeout(() => setIsRefreshing(false), 800); } }; const handleMaximize = async () => { await exitMiniMode(); setMiniView(false); }; const handleMouseDown = () => { if (isTauri()) { getCurrentWindow().startDragging(); } }; // Extract specific models to match AccountRow.tsx const geminiProModel = currentAccount?.quota?.models .filter(m => m.name.toLowerCase() === 'gemini-3-pro-high' || m.name.toLowerCase() === 'gemini-3-pro-low' || m.name.toLowerCase() === 'gemini-3.1-pro-high' || m.name.toLowerCase() === 'gemini-3.1-pro-low' ) .sort((a, b) => (a.percentage || 0) - (b.percentage || 0))[0]; const geminiFlashModel = currentAccount?.quota?.models.find(m => m.name.toLowerCase() === 'gemini-3-flash'); const claudeGroupNames = [ 'claude-opus-4-6-thinking', 'claude' ]; const claudeModel = currentAccount?.quota?.models .filter(m => claudeGroupNames.includes(m.name.toLowerCase())) .sort((a, b) => (a.percentage || 0) - (b.percentage || 0))[0]; // Helper to render a model row const renderModelRow = (model: any, displayName: string, colorClass: string) => { if (!model) return null; // Determine status color based on percentage const getStatusColor = (p: number) => { if (p >= 50) return 'text-emerald-500'; if (p >= 20) return 'text-amber-500'; return 'text-rose-500'; }; const getBarColor = (p: number) => { if (p >= 50) return colorClass === 'cyan' ? 'bg-gradient-to-r from-cyan-400 to-cyan-500' : 'bg-gradient-to-r from-emerald-400 to-emerald-500'; if (p >= 20) return colorClass === 'cyan' ? 'bg-gradient-to-r from-orange-400 to-orange-500' : 'bg-gradient-to-r from-amber-400 to-amber-500'; return 'bg-gradient-to-r from-rose-400 to-rose-500'; }; return (
{displayName}
{model.reset_time ? `R: ${formatTimeRemaining(model.reset_time)}` : t('common.unknown')} {model.percentage}%
); }; return (
{/* Main Container - 300px fixed width */} {/* Header / Drag Region */}
{currentAccount?.email?.split('@')[0] || 'No Account'}
e.stopPropagation()} >
{/* Content Scroll Area */}
{!currentAccount ? (

No account selected

) : (
{/* Account Info Card - Now simplified */}
{/* Custom Label */} {currentAccount.custom_label && ( {currentAccount.custom_label} )}
{/* Divider only if there was content above it */} {currentAccount.custom_label &&
} {/* Models List */}
{renderModelRow(geminiProModel, 'Gemini 3.1 Pro', 'emerald')} {renderModelRow(geminiFlashModel, 'Gemini 3 Flash', 'emerald')} {renderModelRow(claudeModel, t('common.claude_series', 'Claude 系列'), 'cyan')} {!geminiProModel && !geminiFlashModel && !claudeModel && (
No quota data available
)}
)}
{/* Footer Status / Latest Log */}
{latestLog ? ( = 200 && latestLog.status < 400 ? 'bg-emerald-500' : 'bg-red-500'}`}> {latestLog.mapped_model || latestLog.model}
I:{formatCompactNumber(latestLog.input_tokens || 0)} / O:{formatCompactNumber(latestLog.output_tokens || 0)}
{(latestLog.duration / 1000).toFixed(2)}s
) : ( <>
Connected
v{appVersion} )}
); }