Spaces:
Running
Running
| <div x-data="dashboard" class="view-container"> | |
| <!-- Compact Header --> | |
| <div class="flex items-center justify-between gap-4 mb-6"> | |
| <!-- Title with inline subtitle --> | |
| <div class="flex flex-wrap items-center gap-4"> | |
| <h1 class="text-2xl font-bold text-white tracking-tight" x-text="$store.global.t('dashboard')"> | |
| Dashboard | |
| </h1> | |
| <div class="flex items-center h-6 px-3 rounded-full bg-space-800/80 border border-space-border/50 shadow-sm backdrop-blur-sm"> | |
| <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider" | |
| x-text="$store.global.t('systemDesc')"> | |
| CLAUDE PROXY SYSTEM | |
| </span> | |
| </div> | |
| </div> | |
| <!-- Compact Status Indicator --> | |
| <div class="flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-space-900/60 border border-space-border/40 whitespace-nowrap flex-shrink-0"> | |
| <div class="relative flex items-center justify-center"> | |
| <span class="absolute w-1.5 h-1.5 bg-neon-green rounded-full animate-ping opacity-75"></span> | |
| <span class="relative w-1.5 h-1.5 bg-neon-green rounded-full"></span> | |
| </div> | |
| <span class="text-[10px] font-mono text-gray-500 uppercase tracking-wider" x-text="$store.global.t('live')">Live</span> | |
| <span class="text-gray-700">•</span> | |
| <span class="text-[10px] font-mono text-gray-400 tabular-nums" | |
| x-text="new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', second: '2-digit'})"> | |
| </span> | |
| </div> | |
| </div> | |
| <!-- Claude Config Warning Banner --> | |
| <div x-cloak | |
| x-show="$store.settings.showConfigWarning && claudeConfigStatus.checked && claudeConfigStatus.needsApply" | |
| x-transition:enter="transition ease-out duration-300" | |
| x-transition:enter-start="opacity-0 -translate-y-2" | |
| x-transition:enter-end="opacity-100 translate-y-0" | |
| class="alert bg-gradient-to-r from-amber-500/10 to-orange-500/10 border border-amber-500/50 shadow-lg mb-6"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> | |
| </svg> | |
| <div class="flex-1"> | |
| <span class="text-white font-medium" x-text="$store.global.t('claudeConfigNotApplied')">Claude CLI not configured</span> | |
| <span class="text-gray-400 text-sm ml-1" x-text="'- ' + ($store.global.t('claudeConfigNotAppliedDesc') || 'Apply a preset to use the proxy with Claude Code.')"></span> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <button type="button" class="btn btn-sm bg-amber-500 hover:bg-amber-600 border-none text-white gap-1" | |
| @click="goToClaudeSettings()" | |
| :title="$store.global.t('configureNow')"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| </svg> | |
| <span x-text="$store.global.t('configureNow')">Configure</span> | |
| </button> | |
| <button type="button" class="btn btn-sm btn-ghost btn-circle text-gray-400 hover:text-white" | |
| @click="$store.settings.showConfigWarning = false; $store.settings.saveSettings(true)" | |
| aria-label="Dismiss warning"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Skeleton Loading (仅在首次加载时显示) --> | |
| <div x-show="$store.data.initialLoad" class="space-y-6"> | |
| <!-- Skeleton Stats Grid --> | |
| <div class="grid grid-cols-2 sm:grid-cols-5 gap-3"> | |
| <div class="skeleton-stat-card"></div> | |
| <div class="skeleton-stat-card"></div> | |
| <div class="skeleton-stat-card"></div> | |
| <div class="skeleton-stat-card"></div> | |
| <div class="skeleton-stat-card col-span-2 sm:col-span-1"></div> | |
| </div> | |
| <!-- Skeleton Charts --> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <div class="skeleton-chart"></div> | |
| <div class="skeleton-chart"></div> | |
| </div> | |
| </div> | |
| <!-- Actual Content (首次加载完成后显示) --> | |
| <div x-show="!$store.data.initialLoad" class="space-y-6"> | |
| <!-- Stats Grid --> | |
| <div class="grid grid-cols-2 sm:grid-cols-5 gap-2 lg:gap-3"> | |
| <div | |
| class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 hover:border-cyan-500/30 hover:bg-cyan-500/5 transition-all duration-300 group relative cursor-pointer min-w-0" | |
| @click="$store.global.activeTab = 'accounts'" | |
| :title="$store.global.t('clickToViewAllAccounts')"> | |
| <!-- Icon --> | |
| <div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-cyan-400/70 transition-colors"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 sm:w-5 sm:h-5 stroke-current"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0z"> | |
| </path> | |
| </svg> | |
| </div> | |
| <!-- Value --> | |
| <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="stats.total"></div> | |
| <!-- Title --> | |
| <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate" | |
| x-text="$store.global.t('totalAccounts')"></div> | |
| <!-- Desc --> | |
| <div class="stat-desc text-cyan-400/60 text-[10px] truncate flex items-center gap-1"> | |
| <span x-text="$store.global.t('linkedAccounts')" class="truncate"></span> | |
| <svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </div> | |
| <!-- Tiers --> | |
| <div class="flex items-center gap-1 mt-2 text-[10px] font-mono flex-wrap" x-show="stats.subscription"> | |
| <template x-if="stats.subscription?.ultra > 0"> | |
| <span class="px-1.5 py-0.5 rounded bg-yellow-500/10 text-yellow-400 border border-yellow-500/30"> | |
| <span x-text="stats.subscription.ultra"></span> <span x-text="$store.global.t('tierUltra')">Ultra</span> | |
| </span> | |
| </template> | |
| <template x-if="stats.subscription?.pro > 0"> | |
| <span class="px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-400 border border-blue-500/30"> | |
| <span x-text="stats.subscription.pro"></span> <span x-text="$store.global.t('tierPro')">Pro</span> | |
| </span> | |
| </template> | |
| <template x-if="stats.subscription?.free > 0"> | |
| <span class="px-1.5 py-0.5 rounded bg-gray-500/10 text-gray-400 border border-gray-500/30"> | |
| <span x-text="stats.subscription.free"></span> <span x-text="$store.global.t('tierFree')">Free</span> | |
| </span> | |
| </template> | |
| </div> | |
| </div> | |
| <div | |
| class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 hover:border-green-500/30 hover:bg-green-500/5 transition-all duration-300 group relative cursor-pointer min-w-0" | |
| @click="$store.global.activeTab = 'models'" | |
| :title="$store.global.t('clickToViewModels')"> | |
| <div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-green-400/70 transition-colors"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 sm:w-5 sm:h-5 stroke-current"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |
| </svg> | |
| </div> | |
| <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="stats.active"></div> | |
| <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate" | |
| x-text="$store.global.t('active')"></div> | |
| <div class="stat-desc text-green-400/60 text-[10px] truncate flex items-center gap-1"> | |
| <span x-text="$store.global.t('operational')" class="truncate"></span> | |
| <svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </div> | |
| </div> | |
| <div | |
| class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 hover:border-red-500/30 hover:bg-red-500/5 transition-all duration-300 group relative cursor-pointer min-w-0" | |
| @click="$store.global.activeTab = 'accounts'" | |
| :title="$store.global.t('clickToViewLimitedAccounts')"> | |
| <div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-red-500/70 transition-colors"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 sm:w-5 sm:h-5 stroke-current"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |
| </svg> | |
| </div> | |
| <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="stats.limited"></div> | |
| <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate" | |
| x-text="$store.global.t('rateLimited')"></div> | |
| <div class="stat-desc text-red-500/60 text-[10px] truncate flex items-center gap-1"> | |
| <span x-text="$store.global.t('cooldown')" class="truncate"></span> | |
| <svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </div> | |
| </div> | |
| <div | |
| class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 hover:border-orange-500/30 hover:bg-orange-500/5 transition-all duration-300 group relative cursor-pointer min-w-0" | |
| @click="$store.global.activeTab = 'models'" | |
| :title="$store.global.t('clickToViewModels')"> | |
| <div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-orange-500/70 transition-colors"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 sm:w-5 sm:h-5 stroke-current"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path> | |
| </svg> | |
| </div> | |
| <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="stats.modelUsage ? stats.modelUsage.limited : 0"></div> | |
| <div class="stat-title text-gray-500 font-mono text-[10px] lg:text-xs uppercase tracking-wider truncate" | |
| x-text="$store.global.t('quotasDepletedTitle')"></div> | |
| <div class="stat-desc text-orange-500/60 text-[10px] truncate flex items-center gap-1"> | |
| <span x-text="$store.global.t('outOfTracked', {total: stats.modelUsage ? stats.modelUsage.total : 0})" class="truncate"></span> | |
| <svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </div> | |
| </div> | |
| <!-- Global Quota Chart --> | |
| <div | |
| class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 xl:p-4 h-full flex flex-row sm:flex-col items-center justify-between gap-2 overflow-hidden relative group hover:border-space-border/60 transition-colors col-span-2 sm:col-span-1 min-w-0"> | |
| <!-- Chart Container --> | |
| <div class="h-14 w-14 xl:h-16 xl:w-16 relative flex-shrink-0 self-center"> | |
| <canvas id="quotaChart"></canvas> | |
| <div class="absolute inset-0 flex items-center justify-center pointer-events-none"> | |
| <div class="text-[10px] font-bold text-white font-mono" x-text="stats.overallHealth + '%'">%</div> | |
| </div> | |
| </div> | |
| <!-- Legend / Info --> | |
| <div class="flex flex-col justify-center gap-1 flex-grow min-w-0 w-full sm:text-center"> | |
| <div class="flex items-center justify-between sm:justify-center h-full"> | |
| <span class="text-[10px] text-gray-500 uppercase font-mono leading-tight whitespace-normal sm:px-1" | |
| x-text="$store.global.t('globalQuota')">Global Quota</span> | |
| </div> | |
| <!-- Custom Legend --> | |
| <div class="space-y-0.5 sm:flex sm:flex-col sm:items-center w-full"> | |
| <div class="flex items-center justify-between sm:justify-center sm:gap-2 text-[10px] text-gray-400 cursor-pointer hover:text-neon-purple transition-colors group/legend w-full sm:w-auto" | |
| @click="$store.global.activeTab = 'models'; $nextTick(() => { $store.data.filters.family = 'claude'; $store.data.computeQuotaRows(); })" | |
| :title="$store.global.t('clickToFilterClaude')"> | |
| <div class="flex items-center gap-1.5"> | |
| <div class="w-1.5 h-1.5 rounded-full bg-neon-purple flex-shrink-0"></div> | |
| <span class="truncate" x-text="$store.global.t('familyClaude')">Claude</span> | |
| <!-- Hidden arrow on desktop/stacked view to save space --> | |
| <svg class="w-2.5 h-2.5 opacity-0 group-hover/legend:opacity-100 transition-opacity flex-shrink-0 sm:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </div> | |
| </div> | |
| <div class="flex items-center justify-between sm:justify-center sm:gap-2 text-[10px] text-gray-400 cursor-pointer hover:text-neon-green transition-colors group/legend w-full sm:w-auto" | |
| @click="$store.global.activeTab = 'models'; $nextTick(() => { $store.data.filters.family = 'gemini'; $store.data.computeQuotaRows(); })" | |
| :title="$store.global.t('clickToFilterGemini')"> | |
| <div class="flex items-center gap-1.5"> | |
| <div class="w-1.5 h-1.5 rounded-full bg-neon-green flex-shrink-0"></div> | |
| <span class="truncate" x-text="$store.global.t('familyGemini')">Gemini</span> | |
| <!-- Hidden arrow on desktop/stacked view to save space --> | |
| <svg class="w-2.5 h-2.5 opacity-0 group-hover/legend:opacity-100 transition-opacity flex-shrink-0 sm:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> | |
| </svg> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Usage Trend Chart --> | |
| <div class="view-card"> | |
| <!-- Header with Stats and Filter --> | |
| <div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6 mb-8"> | |
| <div class="flex flex-wrap items-center gap-5"> | |
| <div class="flex items-center gap-2.5"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" | |
| class="w-4 h-4 text-neon-purple"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path> | |
| </svg> | |
| <h3 class="text-xs font-mono text-gray-400 uppercase tracking-widest whitespace-nowrap" | |
| x-text="$store.global.t('requestVolume')">Request Volume</h3> | |
| </div> | |
| <!-- Usage Stats Pills --> | |
| <div class="flex flex-wrap gap-2.5 text-[10px] font-mono"> | |
| <div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap"> | |
| <span class="text-gray-500" x-text="$store.global.t('totalColon')">Total:</span> | |
| <span class="text-white ml-1 font-bold" x-text="usageStats.total"></span> | |
| </div> | |
| <div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap"> | |
| <span class="text-gray-500" x-text="$store.global.t('todayColon')">Today:</span> | |
| <span class="text-neon-cyan ml-1 font-bold" x-text="usageStats.today"></span> | |
| </div> | |
| <div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap"> | |
| <span class="text-gray-500" x-text="$store.global.t('hour1Colon')">1H:</span> | |
| <span class="text-neon-green ml-1 font-bold" x-text="usageStats.thisHour"></span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-2 sm:gap-3 w-full sm:w-auto justify-start sm:justify-end flex-wrap lg:flex-nowrap lg:gap-4 lg:bg-space-900/40 lg:p-1.5 lg:rounded-lg lg:border lg:border-space-border/30 lg:whitespace-nowrap lg:flex-shrink-0"> | |
| <!-- Time Range Dropdown --> | |
| <div class="relative flex-1 sm:flex-none"> | |
| <button @click="showTimeRangeDropdown = !showTimeRangeDropdown; showDisplayModeDropdown = false; showModelFilter = false" | |
| class="filter-control"> | |
| <svg class="w-3.5 h-3.5 lg:w-4 lg:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| <span x-text="getTimeRangeLabel()"></span> | |
| <svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showTimeRangeDropdown}" fill="none" | |
| viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </button> | |
| <div x-show="showTimeRangeDropdown" @click.outside="showTimeRangeDropdown = false" | |
| x-transition:enter="transition ease-out duration-100" | |
| x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" | |
| x-transition:leave="transition ease-in duration-75" | |
| x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" | |
| class="absolute right-0 mt-1 w-36 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 py-1" | |
| style="display: none;"> | |
| <button @click="setTimeRange('1h')" class="filter-control-item" | |
| :class="timeRange === '1h' ? 'text-neon-cyan' : 'text-gray-400'" | |
| x-text="$store.global.t('last1Hour')"></button> | |
| <button @click="setTimeRange('6h')" class="filter-control-item" | |
| :class="timeRange === '6h' ? 'text-neon-cyan' : 'text-gray-400'" | |
| x-text="$store.global.t('last6Hours')"></button> | |
| <button @click="setTimeRange('24h')" class="filter-control-item" | |
| :class="timeRange === '24h' ? 'text-neon-cyan' : 'text-gray-400'" | |
| x-text="$store.global.t('last24Hours')"></button> | |
| <button @click="setTimeRange('7d')" class="filter-control-item" | |
| :class="timeRange === '7d' ? 'text-neon-cyan' : 'text-gray-400'" | |
| x-text="$store.global.t('last7Days')"></button> | |
| <button @click="setTimeRange('all')" class="filter-control-item" | |
| :class="timeRange === 'all' ? 'text-neon-cyan' : 'text-gray-400'" | |
| x-text="$store.global.t('allTime')"></button> | |
| </div> | |
| </div> | |
| <!-- Display Mode Dropdown --> | |
| <div class="relative flex-1 sm:flex-none"> | |
| <button @click="showDisplayModeDropdown = !showDisplayModeDropdown; showTimeRangeDropdown = false; showModelFilter = false" | |
| class="flex items-center justify-center gap-2 px-3 py-1.5 lg:px-4 lg:py-2 text-[10px] lg:text-xs font-mono font-medium text-gray-400 bg-space-800 lg:bg-transparent border border-space-border/50 lg:border-transparent rounded lg:rounded-md hover:text-white lg:hover:bg-space-800 hover:border-neon-purple/50 lg:hover:border-neon-purple/30 lg:hover:shadow-lg lg:hover:shadow-neon-purple/10 transition-all duration-200 whitespace-nowrap w-full sm:w-auto"> | |
| <svg class="w-3 h-3 lg:w-4 lg:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> | |
| </svg> | |
| <span x-text="displayMode === 'family' ? $store.global.t('family') : $store.global.t('model')"></span> | |
| <svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showDisplayModeDropdown}" fill="none" | |
| viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </button> | |
| <div x-show="showDisplayModeDropdown" @click.outside="showDisplayModeDropdown = false" | |
| x-transition:enter="transition ease-out duration-100" | |
| x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" | |
| x-transition:leave="transition ease-in duration-75" | |
| x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" | |
| class="absolute right-0 mt-1 w-32 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 py-1" | |
| style="display: none;"> | |
| <button @click="setDisplayMode('family')" class="filter-control-item" | |
| :class="displayMode === 'family' ? 'text-neon-purple' : 'text-gray-400'" | |
| x-text="$store.global.t('family')"></button> | |
| <button @click="setDisplayMode('model')" class="filter-control-item" | |
| :class="displayMode === 'model' ? 'text-neon-purple' : 'text-gray-400'" | |
| x-text="$store.global.t('model')"></button> | |
| </div> | |
| </div> | |
| <!-- Filter Dropdown --> | |
| <div class="relative flex-1 sm:flex-none min-w-[120px]"> | |
| <button @click="showModelFilter = !showModelFilter; showTimeRangeDropdown = false; showDisplayModeDropdown = false" | |
| class="flex items-center justify-center gap-2 px-3 py-1.5 lg:px-4 lg:py-2 text-[10px] lg:text-xs font-mono font-medium text-gray-400 bg-space-800 lg:bg-transparent border border-space-border/50 lg:border-transparent rounded lg:rounded-md hover:text-white lg:hover:bg-space-800 hover:border-neon-purple/50 lg:hover:border-neon-purple/30 lg:hover:shadow-lg lg:hover:shadow-neon-purple/10 transition-all duration-200 whitespace-nowrap w-full sm:w-auto"> | |
| <svg class="w-3 h-3 lg:w-4 lg:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /> | |
| </svg> | |
| <span x-text="$store.global.t('filter') + ' (' + getSelectedCount() + ')'">Filter (0/0)</span> | |
| <svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showModelFilter}" fill="none" | |
| viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </button> | |
| <!-- Dropdown Menu --> | |
| <div x-show="showModelFilter" @click.outside="showModelFilter = false" | |
| x-transition:enter="transition ease-out duration-100" | |
| x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" | |
| x-transition:leave="transition ease-in duration-75" | |
| x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" | |
| class="absolute right-0 mt-1 w-72 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 overflow-hidden" | |
| style="display: none;"> | |
| <!-- Header --> | |
| <div | |
| class="flex items-center justify-between px-3 py-2 border-b border-space-border/50 bg-space-800/50"> | |
| <span class="text-[10px] font-mono text-gray-500 uppercase" | |
| x-text="displayMode === 'family' ? $store.global.t('selectFamilies') : $store.global.t('selectModels')"></span> | |
| <div class="flex gap-1"> | |
| <button @click="autoSelectTopN(5)" class="text-[10px] text-neon-purple hover:underline" | |
| :title="$store.global.t('smartTitle')" x-text="$store.global.t('frequentModels')"> | |
| Smart | |
| </button> | |
| <span class="text-gray-600">|</span> | |
| <button @click="selectAll()" class="text-[10px] text-neon-cyan hover:underline" | |
| x-text="$store.global.t('all')">All</button> | |
| <span class="text-gray-600">|</span> | |
| <button @click="deselectAll()" class="text-[10px] text-gray-500 hover:underline" | |
| x-text="$store.global.t('none')">None</button> | |
| </div> | |
| </div> | |
| <!-- Hierarchical List --> | |
| <div class="max-h-64 overflow-y-auto p-2 space-y-2"> | |
| <template x-for="family in families" :key="family"> | |
| <div class="space-y-1"> | |
| <!-- Family Header --> | |
| <label | |
| class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-white/5 cursor-pointer group" | |
| x-show="displayMode === 'family'"> | |
| <input type="checkbox" :checked="isFamilySelected(family)" | |
| @change="toggleFamily(family)" | |
| class="checkbox checkbox-xs checkbox-primary"> | |
| <div class="w-2 h-2 rounded-full flex-shrink-0" | |
| :style="'background-color:' + getFamilyColor(family)"></div> | |
| <span class="text-xs text-gray-300 font-medium group-hover:text-white" | |
| x-text="$store.global.t('family' + family.charAt(0).toUpperCase() + family.slice(1))"></span> | |
| <span class="text-[10px] text-gray-600 ml-auto" | |
| x-text="'(' + (modelTree[family] || []).length + ')'"></span> | |
| </label> | |
| <!-- Family Section Header (Model Mode) --> | |
| <div class="flex items-center gap-2 px-2 py-1 text-[10px] text-gray-500 uppercase font-bold" | |
| x-show="displayMode === 'model'"> | |
| <div class="w-1.5 h-1.5 rounded-full" | |
| :style="'background-color:' + getFamilyColor(family)"></div> | |
| <span | |
| x-text="$store.global.t('family' + family.charAt(0).toUpperCase() + family.slice(1))"></span> | |
| </div> | |
| <!-- Models in Family --> | |
| <template x-if="displayMode === 'model'"> | |
| <div class="ml-4 space-y-0.5"> | |
| <template x-for="(model, modelIndex) in (modelTree[family] || [])" | |
| :key="family + ':' + model"> | |
| <label | |
| class="flex items-center gap-2 px-2 py-1 rounded hover:bg-white/5 cursor-pointer group"> | |
| <input type="checkbox" :checked="isModelSelected(family, model)" | |
| @change="toggleModel(family, model)" | |
| class="checkbox checkbox-xs checkbox-primary"> | |
| <div class="w-2 h-2 rounded-full flex-shrink-0" | |
| :style="'background-color:' + getModelColor(family, modelIndex)"> | |
| </div> | |
| <span class="text-xs text-gray-400 truncate group-hover:text-white" | |
| x-text="model"></span> | |
| </label> | |
| </template> | |
| </div> | |
| </template> | |
| </div> | |
| </template> | |
| <!-- Empty State --> | |
| <div x-show="families.length === 0" class="text-center py-4 text-gray-600 text-xs" | |
| x-text="$store.global.t('noDataTracked')"> | |
| No data tracked yet | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Dynamic Legend --> | |
| <div class="flex flex-wrap gap-3 mb-5" | |
| x-show="displayMode === 'family' ? selectedFamilies.length > 0 : Object.values(selectedModels).flat().length > 0"> | |
| <!-- Family Mode Legend --> | |
| <template x-if="displayMode === 'family'"> | |
| <template x-for="family in selectedFamilies" :key="family"> | |
| <div class="flex items-center gap-1.5 text-[10px] font-mono"> | |
| <div class="w-2 h-2 rounded-full" :style="'background-color:' + getFamilyColor(family)"></div> | |
| <span class="text-gray-400" | |
| x-text="$store.global.t('family' + family.charAt(0).toUpperCase() + family.slice(1))"></span> | |
| </div> | |
| </template> | |
| </template> | |
| <!-- Model Mode Legend --> | |
| <template x-if="displayMode === 'model'"> | |
| <template x-for="family in families" :key="'legend-' + family"> | |
| <template x-for="(model, modelIndex) in (selectedModels[family] || [])" :key="family + ':' + model"> | |
| <div class="flex items-center gap-1.5 text-[10px] font-mono"> | |
| <div class="w-2 h-2 rounded-full" | |
| :style="'background-color:' + getModelColor(family, modelIndex)"></div> | |
| <span class="text-gray-400" x-text="model"></span> | |
| </div> | |
| </template> | |
| </template> | |
| </template> | |
| </div> | |
| <!-- Chart --> | |
| <div class="h-48 w-full relative"> | |
| <canvas id="usageTrendChart"></canvas> | |
| <!-- Overall Loading State --> | |
| <div x-show="!stats.hasTrendData" | |
| class="absolute inset-0 flex items-center justify-center bg-space-900/50 backdrop-blur-sm z-10" | |
| style="display: none;"> | |
| <div class="text-xs font-mono text-gray-500 flex items-center gap-2"> | |
| <span class="loading loading-spinner loading-xs"></span> | |
| <span x-text="$store.global.t('syncing')">SYNCING...</span> | |
| </div> | |
| </div> | |
| <!-- Empty State (After Filtering) --> | |
| <div x-show="stats.hasTrendData && !hasFilteredTrendData" | |
| class="absolute inset-0 flex flex-col items-center justify-center bg-space-900/30 z-10" | |
| style="display: none;"> | |
| <div class="flex flex-col items-center gap-4 animate-fade-in"> | |
| <div class="w-12 h-12 rounded-full bg-space-850 flex items-center justify-center text-gray-600 border border-space-border/50"> | |
| <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> | |
| </svg> | |
| </div> | |
| <div class="text-xs font-mono text-gray-500 text-center"> | |
| <p x-text="$store.global.t('noDataTracked')">No data tracked yet</p> | |
| <p class="text-[10px] opacity-60 mt-1" x-text="'[' + getTimeRangeLabel() + ']'"></p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- No Selection State --> | |
| <div x-show="stats.hasTrendData && hasFilteredTrendData && (displayMode === 'family' ? selectedFamilies.length === 0 : Object.values(selectedModels).flat().length === 0)" | |
| class="absolute inset-0 flex items-center justify-center bg-space-900/30 z-10" | |
| style="display: none;"> | |
| <div class="text-xs font-mono text-gray-500" | |
| x-text="displayMode === 'family' ? $store.global.t('selectFamilies') : $store.global.t('selectModels')"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> <!-- End of x-show="!$store.data.loading" --> | |
| </div> |