Spaces:
Running
Running
| <div x-data="models" 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('models')"> | |
| Models | |
| </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('modelsPageDesc')"> | |
| Real-time quota and status for all available models. | |
| </span> | |
| </div> | |
| </div> | |
| <!-- Search Bar --> | |
| <div class="relative w-72 h-9"> | |
| <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | |
| <svg class="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> | |
| </svg> | |
| </div> | |
| <input type="text" | |
| :placeholder="$store.global.t('searchPlaceholder')" | |
| :aria-label="$store.global.t('searchPlaceholder')" | |
| class="input-search-sm pr-10" | |
| x-model.debounce="$store.data.filters.search" | |
| @input="$store.data.computeQuotaRows()"> | |
| <button x-show="$store.data.filters.search" | |
| x-transition:enter="transition ease-out duration-100" | |
| x-transition:enter-start="opacity-0 scale-75" | |
| x-transition:enter-end="opacity-100 scale-100" | |
| @click="$store.data.filters.search = ''; $store.data.computeQuotaRows()" | |
| class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white transition-colors"> | |
| <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Controls --> | |
| <div class="view-card !p-4 flex flex-col lg:flex-row items-center justify-between gap-4"> | |
| <div class="flex flex-col md:flex-row items-center gap-4 w-full lg:w-auto flex-wrap"> | |
| <!-- Custom Select --> | |
| <div class="relative w-full md:w-64 h-9"> | |
| <select | |
| class="w-full h-full bg-space-800 border border-space-border text-gray-300 rounded-lg pl-4 pr-10 focus:outline-none focus:border-neon-purple focus:ring-1 focus:ring-neon-purple transition-all truncate text-xs cursor-pointer" | |
| style="appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: none;" | |
| x-model="$store.data.filters.account" @change="$store.data.computeQuotaRows()"> | |
| <option value="all" x-text="$store.global.t('allAccounts')">All Accounts</option> | |
| <template x-for="acc in $store.data.accounts" :key="acc.email"> | |
| <option :value="acc.email" x-text="Redact.email(acc.email).split('@')[0]"></option> | |
| </template> | |
| </select> | |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-500"> | |
| <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </div> | |
| </div> | |
| <!-- Filter Buttons --> | |
| <div class="join h-9 w-full md:w-auto overflow-x-auto"> | |
| <button | |
| class="join-item btn btn-xs h-full flex-1 md:flex-none px-6 border-space-border/50 bg-space-800 transition-all font-medium text-[10px] tracking-wide" | |
| :class="$store.data.filters.family === 'all' | |
| ? 'bg-neon-purple/20 text-neon-purple border-neon-purple/60 shadow-lg shadow-neon-purple/10' | |
| : 'text-gray-400 hover:text-white hover:bg-space-700 hover:border-space-border'" | |
| @click="$store.data.filters.family = 'all'; $store.data.computeQuotaRows()" | |
| x-text="$store.global.t('allCaps')">ALL</button> | |
| <button | |
| class="join-item btn btn-xs h-full flex-1 md:flex-none px-6 border-space-border/50 bg-space-800 transition-all font-medium text-[10px] tracking-wide" | |
| :class="$store.data.filters.family === 'claude' | |
| ? 'bg-neon-purple/20 text-neon-purple border-neon-purple/60 shadow-lg shadow-neon-purple/10' | |
| : 'text-gray-400 hover:text-white hover:bg-space-700 hover:border-space-border'" | |
| @click="$store.data.filters.family = 'claude'; $store.data.computeQuotaRows()" | |
| x-text="$store.global.t('claudeCaps')">CLAUDE</button> | |
| <button | |
| class="join-item btn btn-xs h-full flex-1 md:flex-none px-6 border-space-border/50 bg-space-800 transition-all font-medium text-[10px] tracking-wide" | |
| :class="$store.data.filters.family === 'gemini' | |
| ? 'bg-neon-green/20 text-neon-green border-neon-green/60 shadow-lg shadow-neon-green/10' | |
| : 'text-gray-400 hover:text-white hover:bg-space-700 hover:border-space-border'" | |
| @click="$store.data.filters.family = 'gemini'; $store.data.computeQuotaRows()" | |
| x-text="$store.global.t('geminiCaps')">GEMINI</button> | |
| </div> | |
| <!-- Show Hidden Toggle --> | |
| <button | |
| class="btn h-9 px-4 ml-2 border border-space-border/50 bg-space-800 hover:bg-space-700 hover:border-space-border hover:text-white transition-all gap-2 min-h-0" | |
| :class="$store.settings.showHiddenModels ? 'text-neon-purple border-neon-purple/50 bg-neon-purple/10' : 'text-gray-400'" | |
| @click="$store.settings.toggle('showHiddenModels'); $store.data.computeQuotaRows()" | |
| :title="$store.settings.showHiddenModels ? $store.global.t('hideHidden') : $store.global.t('showHidden')"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" /> | |
| </svg> | |
| <span class="text-[10px] font-medium uppercase tracking-wide" x-text="$store.settings.showHiddenModels ? $store.global.t('hiddenOn') : $store.global.t('hiddenOff')"></span> | |
| </button> | |
| <!-- Show All Accounts Toggle (only in detail mode) --> | |
| <button x-show="!$store.settings.compact" | |
| class="btn h-9 px-4 ml-2 border border-space-border/50 bg-space-800 hover:bg-space-700 hover:border-space-border hover:text-white transition-all gap-2 min-h-0" | |
| :class="$store.settings.showAllAccounts ? 'text-neon-purple border-neon-purple/50 bg-neon-purple/10' : 'text-gray-400'" | |
| @click="$store.settings.toggle('showAllAccounts')" | |
| :title="$store.settings.showAllAccounts ? 'Cap account rows' : 'Show all account rows'"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <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 0z" /> | |
| </svg> | |
| <span class="text-[10px] font-medium uppercase tracking-wide" x-text="$store.settings.showAllAccounts ? 'All' : 'Capped'"></span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Main Table Card --> | |
| <div class="view-card !p-0"> | |
| <div class="overflow-x-auto min-h-[400px]"> | |
| <table class="standard-table" | |
| :class="{'table-xs': $store.settings.compact, 'table-sm': !$store.settings.compact}"> | |
| <thead> | |
| <tr> | |
| <th class="w-14 !pl-4 whitespace-nowrap cursor-pointer hover:text-white transition-colors select-none group" | |
| @click="$store.data.setSort('avgQuota')"> | |
| <div class="flex items-center justify-center"> | |
| <span x-text="$store.global.t('stat')">Stat</span> | |
| </div> | |
| </th> | |
| <th class="whitespace-nowrap cursor-pointer hover:text-white transition-colors select-none group" | |
| @click="$store.data.setSort('modelId')"> | |
| <div class="flex items-center gap-1"> | |
| <span x-text="$store.global.t('modelIdentity')">Model Identity</span> | |
| <svg class="w-3 h-3 transition-colors" | |
| :class="$store.data.filters.sortCol === 'modelId' ? 'text-neon-purple' : 'text-gray-700 opacity-0 group-hover:opacity-100'" | |
| fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| :d="$store.data.filters.sortCol === 'modelId' ? ($store.data.filters.sortAsc ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7') : 'M8 9l4-4 4 4m0 6l-4 4-4-4'" /> | |
| </svg> | |
| </div> | |
| </th> | |
| <th class="min-w-[12rem] whitespace-nowrap cursor-pointer hover:text-white transition-colors select-none group" | |
| @click="$store.data.setSort('avgQuota')"> | |
| <div class="flex items-center gap-1"> | |
| <span x-text="$store.global.t('globalQuota')">Global Quota</span> | |
| <svg class="w-3 h-3 transition-colors" | |
| :class="$store.data.filters.sortCol === 'avgQuota' ? 'text-neon-purple' : 'text-gray-700 opacity-0 group-hover:opacity-100'" | |
| fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| :d="$store.data.filters.sortCol === 'avgQuota' ? ($store.data.filters.sortAsc ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7') : 'M8 9l4-4 4 4m0 6l-4 4-4-4'" /> | |
| </svg> | |
| </div> | |
| </th> | |
| <th x-show="!$store.settings.compact" | |
| class="min-w-[8rem] whitespace-nowrap cursor-pointer hover:text-white transition-colors select-none group" | |
| @click="$store.data.setSort('minResetTime')"> | |
| <div class="flex items-center gap-1"> | |
| <span x-text="$store.global.t('nextReset')">Next Reset</span> | |
| <svg class="w-3 h-3 transition-colors" | |
| :class="$store.data.filters.sortCol === 'minResetTime' ? 'text-neon-purple' : 'text-gray-700 opacity-0 group-hover:opacity-100'" | |
| fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| :d="$store.data.filters.sortCol === 'minResetTime' ? ($store.data.filters.sortAsc ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7') : 'M8 9l4-4 4 4m0 6l-4 4-4-4'" /> | |
| </svg> | |
| </div> | |
| </th> | |
| <th x-show="!$store.settings.compact" | |
| class="whitespace-nowrap cursor-pointer hover:text-white transition-colors select-none group" | |
| @click="$store.data.setSort('activeCount')"> | |
| <div class="flex items-center gap-1 justify-start"> | |
| <span x-text="$store.global.t('distribution')">Account Distribution</span> | |
| <svg class="w-3 h-3 transition-colors" | |
| :class="$store.data.filters.sortCol === 'activeCount' ? 'text-neon-purple' : 'text-gray-700 opacity-0 group-hover:opacity-100'" | |
| fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| :d="$store.data.filters.sortCol === 'activeCount' ? ($store.data.filters.sortAsc ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7') : 'M8 9l4-4 4 4m0 6l-4 4-4-4'" /> | |
| </svg> | |
| </div> | |
| </th> | |
| <th class="w-20 !pr-4 text-right whitespace-nowrap" x-text="$store.global.t('actions')">Actions | |
| </th> | |
| </tr> | |
| </thead> | |
| <tbody class="text-sm"> | |
| <template x-for="row in $store.data.quotaRows" :key="row.modelId"> | |
| <tr class="group transition-all duration-300" :class="row.hidden ? 'opacity-40 grayscale hover:opacity-100 hover:grayscale-0 bg-space-900/50' : ''"> | |
| <td class="!pl-4"> | |
| <div class="w-2 h-2 rounded-full transition-all duration-500" | |
| :class="row.avgQuota > 0 ? 'bg-neon-green shadow-[0_0_8px_rgba(34,197,94,0.6)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.6)]'"> | |
| </div> | |
| </td> | |
| <td> | |
| <div class="font-bold text-gray-200 group-hover:text-neon-purple transition-colors" | |
| :class="$store.settings.compact ? 'text-xs' : ''" | |
| x-text="row.modelId"></div> | |
| <div class="text-[10px] font-mono text-gray-500 uppercase" | |
| x-show="!$store.settings.compact" | |
| x-text="$store.global.t('family' + row.family.charAt(0).toUpperCase() + row.family.slice(1))"> | |
| </div> | |
| </td> | |
| <td> | |
| <div class="flex flex-col pr-4" | |
| :class="$store.settings.compact ? 'gap-0.5' : 'gap-1'"> | |
| <div class="flex justify-between text-xs font-mono"> | |
| <span class="flex items-center gap-1.5"> | |
| <span x-text="row.avgQuota + '%'" | |
| :class="row.avgQuota > 0 ? 'text-white' : 'text-red-500'"></span> | |
| <!-- Threshold indicator (inline) --> | |
| <span x-show="row.effectiveThresholdPct > 0" | |
| class="text-[10px] text-gray-500" | |
| x-text="row.hasVariedThresholds && row.quotaInfo.some(q => q.thresholdPct > 0) | |
| ? '· min ' + Math.min(...row.quotaInfo.map(q => q.thresholdPct).filter(t => t > 0)) + '–' + row.effectiveThresholdPct + '%' | |
| : '· min ' + row.effectiveThresholdPct + '%'"></span> | |
| <!-- Inline reset time (compact only) --> | |
| <span x-show="$store.settings.compact && row.resetIn && row.resetIn !== '-'" | |
| class="text-[10px]" | |
| :class="row.resetIn.indexOf('h') === -1 ? 'text-neon-purple' : 'text-gray-500'" | |
| x-text="'· ' + row.resetIn"></span> | |
| </span> | |
| <span class="flex items-center gap-2"> | |
| <!-- Compact: inline account squares --> | |
| <span x-show="$store.settings.compact && row.quotaInfo && row.quotaInfo.length > 0" | |
| class="inline-flex gap-0.5 items-center"> | |
| <template x-for="q in row.quotaInfo.slice(0, 8)" :key="q.fullEmail + '-sq'"> | |
| <span class="w-1.5 h-1.5 rounded-sm inline-block" | |
| :class="q.pct > 50 ? 'bg-neon-green/80' : (q.pct > 0 ? 'bg-yellow-500/80' : 'bg-red-900/60')"></span> | |
| </template> | |
| <span x-show="row.quotaInfo.length > 8" | |
| class="text-[8px] text-gray-600 leading-none" | |
| x-text="'+' + (row.quotaInfo.length - 8)"></span> | |
| </span> | |
| <!-- Available/Total Accounts Indicator --> | |
| <span class="text-gray-500 text-[10px]" | |
| x-text="row.quotaInfo.filter(q => q.pct > 0).length + '/' + row.quotaInfo.length"></span> | |
| </span> | |
| </div> | |
| <div class="relative"> | |
| <progress class="progress w-full bg-space-800" | |
| :class="[ | |
| $store.settings.compact ? 'h-1' : 'h-1.5', | |
| row.avgQuota > 50 ? 'progress-gradient-success' : (row.avgQuota > 0 ? 'progress-gradient-warning' : 'progress-gradient-error') | |
| ]" | |
| :value="row.avgQuota" max="100"></progress> | |
| <!-- Per-account draggable quota threshold markers --> | |
| <template x-for="(q, qIdx) in row.quotaInfo.filter(q => q.thresholdPct > 0 || isDragging(q, row))" :key="q.fullEmail + '-threshold'"> | |
| <div class="absolute -top-1 -bottom-1 group/marker" | |
| :class="isDragging(q, row) ? 'cursor-grabbing z-30' : 'cursor-grab'" | |
| :style="'left: calc(' + getMarkerPct(q, row) + '% - 4px); width: 9px; transform: translateX(' + (isDragging(q, row) ? '0px' : getMarkerOffset(q, row, qIdx)) + ');'" | |
| :title="Redact.email(q.email) + ' min: ' + getMarkerPct(q, row) + '%'" | |
| @mousedown.prevent="startDrag($event, q, row)" | |
| @touchstart.prevent="startDrag($event, q, row)"> | |
| <!-- Visible marker line --> | |
| <div class="absolute top-1 bottom-1 left-1 w-[3px] rounded-full" | |
| :class="isDragging(q, row) ? 'scale-y-150 brightness-150' : 'transition-all group-hover/marker:scale-y-150 group-hover/marker:brightness-125'" | |
| :style="'background-color: ' + getThresholdColor(qIdx).bg + '; box-shadow: 0 0 ' + (isDragging(q, row) ? '10px' : '6px') + ' ' + getThresholdColor(qIdx).shadow"></div> | |
| <!-- Tooltip popup (visible on hover or during drag) --> | |
| <div class="absolute -top-8 left-1/2 -translate-x-1/2 items-center gap-1.5 | |
| whitespace-nowrap text-xs font-mono font-semibold px-2.5 py-1 rounded-md bg-space-900 border border-space-border shadow-xl shadow-black/40 z-20" | |
| :class="isDragging(q, row) ? 'flex' : 'hidden group-hover/marker:flex'" | |
| :style="'color: ' + getThresholdColor(qIdx).bg + '; border-color: ' + getThresholdColor(qIdx).bg + '40'"> | |
| <span class="w-2 h-2 rounded-full flex-shrink-0" :style="'background-color: ' + getThresholdColor(qIdx).bg"></span> | |
| <span x-text="Redact.email(q.email)"></span> | |
| <span class="text-gray-500">min</span> | |
| <span x-text="getMarkerPct(q, row) + '%'"></span> | |
| </div> | |
| </div> | |
| </template> | |
| </div> | |
| <!-- Per-account breakdown (non-compact only) --> | |
| <div x-show="!$store.settings.compact && row.quotaInfo && row.quotaInfo.length > 1" | |
| class="flex flex-col gap-1 mt-1 pt-1 border-t border-space-border/30"> | |
| <template x-for="q in getVisibleAccounts(row)" :key="q.fullEmail + '-detail'"> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-[10px] font-mono text-gray-500 w-20 truncate text-right flex-shrink-0" | |
| x-text="Redact.email(q.email)"></span> | |
| <div class="flex-1 h-1 rounded-full bg-space-800 overflow-hidden"> | |
| <div class="h-full rounded-full transition-all duration-500" | |
| :class="q.pct > 50 ? 'bg-neon-green/70' : (q.pct > 0 ? 'bg-yellow-500/70' : 'bg-red-500/50')" | |
| :style="'width: ' + q.pct + '%'"></div> | |
| </div> | |
| <span class="text-[10px] font-mono w-8 text-right flex-shrink-0" | |
| :class="q.pct > 0 ? 'text-gray-400' : 'text-red-500/70'" | |
| x-text="q.pct + '%'"></span> | |
| <span x-show="q.resetTime" | |
| class="text-[9px] font-mono text-gray-600 w-12 text-right flex-shrink-0" | |
| x-text="window.utils.formatTimeUntil(q.resetTime)"></span> | |
| </div> | |
| </template> | |
| <!-- "+N more" expand button --> | |
| <button x-show="getHiddenCount(row) > 0" | |
| @click="toggleExpanded(row.modelId)" | |
| class="text-[10px] font-mono text-neon-purple/70 hover:text-neon-purple transition-colors text-left pl-[88px] py-0.5 cursor-pointer" | |
| x-text="'+' + getHiddenCount(row) + ' more'"></button> | |
| <!-- "Show less" collapse button --> | |
| <button x-show="isExpanded(row.modelId) && !$store.settings.showAllAccounts && row.quotaInfo.length > window.AppConstants.LIMITS.ACCOUNT_BREAKDOWN_LIMIT" | |
| @click="toggleExpanded(row.modelId)" | |
| class="text-[10px] font-mono text-gray-500 hover:text-gray-300 transition-colors text-left pl-[88px] py-0.5 cursor-pointer">Show less</button> | |
| </div> | |
| </div> | |
| </td> | |
| <td x-show="!$store.settings.compact" class="font-mono text-xs"> | |
| <div class="tooltip tooltip-left" | |
| :data-tip="row.quotaInfo && row.quotaInfo.length > 0 ? row.quotaInfo.filter(q => q.resetTime).map(q => Redact.email(q.email) + ': ' + q.resetTime).join('\n') : 'No reset data'"> | |
| <span x-text="row.resetIn" | |
| :class="(row.resetIn && row.resetIn.indexOf('h') === -1 && row.resetIn !== '-') ? 'text-neon-purple font-bold' : 'text-gray-400'"></span> | |
| </div> | |
| </td> | |
| <td x-show="!$store.settings.compact"> | |
| <div class="flex items-center justify-start gap-3"> | |
| <div class="text-[10px] font-mono text-gray-500 hidden xl:block text-left leading-tight opacity-70"> | |
| <div | |
| x-text="$store.global.t('activeCount', {count: row.quotaInfo?.filter(q => q.pct > 0).length || 0})"> | |
| </div> | |
| </div> | |
| <!-- Account Status Indicators --> | |
| <div class="flex flex-wrap gap-1 justify-start max-w-[200px]" x-data="{ maxVisible: 12 }"> | |
| <template x-if="!row.quotaInfo || row.quotaInfo.length === 0"> | |
| <div class="text-[10px] text-gray-600 italic" x-text="$store.global.t('noData')">No data</div> | |
| </template> | |
| <template x-if="row.quotaInfo && row.quotaInfo.length > 0"> | |
| <div class="flex flex-wrap gap-1 justify-start"> | |
| <!-- Visible accounts (limited to maxVisible) --> | |
| <template x-for="(q, idx) in row.quotaInfo.slice(0, maxVisible)" :key="q.fullEmail"> | |
| <div class="tooltip tooltip-left" | |
| :data-tip="`${Redact.email(q.fullEmail)} (${q.pct}%)` + (q.thresholdPct > 0 ? ` · min: ${q.thresholdPct}%` : '')"> | |
| <div class="w-2.5 h-2.5 rounded-[2px] transition-all hover:scale-125 cursor-help" | |
| :class="q.pct > 50 ? 'bg-neon-green opacity-80' : (q.pct > 0 ? 'bg-yellow-500 opacity-80' : 'bg-red-900 opacity-50')"> | |
| </div> | |
| </div> | |
| </template> | |
| <!-- Overflow indicator --> | |
| <template x-if="row.quotaInfo.length > maxVisible"> | |
| <div class="tooltip tooltip-left" | |
| :data-tip="row.quotaInfo.slice(maxVisible).map(q => `${Redact.email(q.fullEmail)} (${q.pct}%)` + (q.thresholdPct > 0 ? ` · min: ${q.thresholdPct}%` : '')).join('\n')"> | |
| <div class="w-2.5 h-2.5 rounded-[2px] bg-gray-700/50 border border-gray-600 flex items-center justify-center cursor-help hover:bg-gray-600/70 transition-colors"> | |
| <span class="text-[6px] text-gray-400 font-bold leading-none" | |
| x-text="`+${row.quotaInfo.length - maxVisible}`"></span> | |
| </div> | |
| </div> | |
| </template> | |
| </div> | |
| </template> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="text-right !pr-4"> | |
| <div | |
| class="flex items-center justify-end gap-1 opacity-50 group-hover:opacity-100 transition-opacity"> | |
| <!-- Pin Toggle --> | |
| <button class="btn btn-xs btn-circle transition-colors" | |
| :class="row.pinned ? 'bg-neon-purple/20 text-neon-purple border-neon-purple/50 hover:bg-neon-purple/30' : 'btn-ghost text-gray-600 hover:text-gray-300'" | |
| @click="await updateModelConfig(row.modelId, { pinned: !row.pinned })" | |
| @keydown.enter="await updateModelConfig(row.modelId, { pinned: !row.pinned })" | |
| @keydown.space.prevent="await updateModelConfig(row.modelId, { pinned: !row.pinned })" | |
| :title="$store.global.t('pinToTop')" | |
| :aria-label="row.pinned ? 'Unpin model ' + row.modelId : 'Pin model ' + row.modelId + ' to top'" | |
| :aria-pressed="row.pinned ? 'true' : 'false'" | |
| tabindex="0"> | |
| <svg x-show="row.pinned" xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" | |
| viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> | |
| <path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z" /> | |
| </svg> | |
| <svg x-show="!row.pinned" xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" | |
| fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /> | |
| </svg> | |
| </button> | |
| <!-- Hide Toggle --> | |
| <button class="btn btn-xs btn-circle transition-colors" | |
| :class="row.hidden ? 'bg-red-500/20 text-red-400 border-red-500/50 hover:bg-red-500/30' : 'btn-ghost text-gray-400 hover:text-white'" | |
| @click="await updateModelConfig(row.modelId, { hidden: !row.hidden })" | |
| @keydown.enter="await updateModelConfig(row.modelId, { hidden: !row.hidden })" | |
| @keydown.space.prevent="await updateModelConfig(row.modelId, { hidden: !row.hidden })" | |
| :title="$store.global.t('toggleVisibility')" | |
| :aria-label="row.hidden ? 'Show model ' + row.modelId : 'Hide model ' + row.modelId" | |
| :aria-pressed="row.hidden ? 'true' : 'false'" | |
| tabindex="0"> | |
| <svg x-show="!row.hidden" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" | |
| fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> | |
| </svg> | |
| <svg x-show="row.hidden" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" | |
| fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| </template> | |
| <!-- Loading --> | |
| <tr x-show="$store.data.loading && !$store.data.quotaRows.length"> | |
| <td :colspan="$store.settings.compact ? 4 : 6" class="h-64 text-center"> | |
| <div class="flex flex-col items-center justify-center gap-3"> | |
| <span class="loading loading-bars loading-md text-neon-purple"></span> | |
| <span class="text-xs font-mono text-gray-600 animate-pulse" | |
| x-text="$store.global.t('establishingUplink')">ESTABLISHING | |
| UPLINK...</span> | |
| </div> | |
| </td> | |
| </tr> | |
| <!-- Empty State --> | |
| <tr x-show="!$store.data.initialLoad && $store.data.quotaRows.length === 0"> | |
| <td :colspan="$store.settings.compact ? 4 : 6" class="py-16 text-center"> | |
| <div class="flex flex-col items-center gap-4 max-w-lg mx-auto"> | |
| <svg class="w-20 h-20 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" | |
| d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" /> | |
| </svg> | |
| <h3 class="text-xl font-semibold text-gray-400" x-text="$store.global.t('noSignal')"> | |
| NO SIGNAL DETECTED | |
| </h3> | |
| <p class="text-sm text-gray-600 max-w-md leading-relaxed"> | |
| No model data available. Add accounts to see available models and quota information. | |
| </p> | |
| <button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-sm gap-2 shadow-lg shadow-neon-purple/20" | |
| @click="$store.global.activeTab = 'accounts'"> | |
| <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="M12 4v16m8-8H4" /> | |
| </svg> | |
| <span x-text="$store.global.t('goToAccounts')">Go to Accounts</span> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> |