style(webui): refine UI polish and enhance component interactions

This commit is contained in:
Wha1eChai
2026-01-09 07:54:50 +08:00
parent 40a766ded6
commit e909ea6fe3
13 changed files with 808 additions and 253 deletions

View File

@@ -1,25 +1,40 @@
<div x-data="accountManager" class="view-container">
<!-- Header -->
<div class="flex items-start justify-between mb-6">
<div>
<h2 class="text-2xl font-bold text-white tracking-tight mb-1" x-text="$store.global.t('accessCredentials')">
<!-- Compact Header -->
<div class="flex items-center justify-between gap-4 mb-6">
<!-- Title with inline subtitle -->
<div class="flex items-baseline gap-3">
<h1 class="text-2xl font-bold text-white tracking-tight" x-text="$store.global.t('accessCredentials')">
Access Credentials
</h2>
<p class="text-sm text-gray-500" x-text="$store.global.t('manageTokens')">
</h1>
<span class="text-[10px] font-mono text-gray-600 uppercase tracking-[0.15em]"
x-text="$store.global.t('manageTokens')">
Manage OAuth tokens and session states
</p>
</span>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-2">
<button class="btn btn-sm btn-outline border-space-border text-gray-400 hover:text-white hover:border-white transition-all gap-2"
<!-- Search -->
<div class="relative" x-show="$store.data.accounts.length > 0">
<input type="text"
x-model="searchQuery"
:placeholder="$store.global.t('searchAccounts')"
class="input input-sm input-bordered bg-space-800 border-space-border text-white w-48 pl-9 text-xs h-8"
@keydown.escape="searchQuery = ''">
<svg class="w-4 h-4 absolute left-3 top-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
<button class="btn btn-xs btn-outline border-space-border text-gray-400 hover:text-white transition-all gap-2 h-8"
@click="reloadAccounts()">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span x-text="$store.global.t('reload')">Reload</span>
</button>
<button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-sm gap-2 shadow-lg shadow-neon-purple/20"
<button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-xs gap-2 shadow-lg shadow-neon-purple/20 h-8"
onclick="document.getElementById('add_account_modal').showModal()">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-3.5 h-3.5" 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('addNode')">Add Node</span>
@@ -27,21 +42,54 @@
</div>
</div>
<!-- Table Container -->
<div class="bg-space-900/40 border border-space-border/30 rounded-xl overflow-hidden backdrop-blur-sm">
<!-- Table Card -->
<div class="view-card !p-0">
<table class="w-full">
<thead>
<tr class="border-b border-space-border/50">
<th class="pl-6 py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-20" x-text="$store.global.t('enabled')">Enabled</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider" x-text="$store.global.t('identity')">Identity (Email)</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-24" x-text="$store.global.t('source')">Source</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-32" x-text="$store.global.t('projectId')">Project ID</th>
<thead x-show="$store.data.accounts.length > 0">
<tr class="bg-space-900/50 border-b border-space-border/50">
<th class="pl-6 py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-16" x-text="$store.global.t('enabled')">Enabled</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider flex-1 min-w-[200px]" x-text="$store.global.t('identity')">Identity (Email)</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-20" x-text="$store.global.t('source')">Source</th>
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-24" x-text="$store.global.t('health')">Health</th>
<th class="py-3 pr-6 text-right text-[10px] font-bold text-gray-500 uppercase tracking-wider w-32" x-text="$store.global.t('operations')">Operations</th>
</tr>
</thead>
<tbody>
<template x-for="acc in $store.data.accounts" :key="acc.email">
<!-- Empty State -->
<template x-if="$store.data.accounts.length === 0">
<tr>
<td colspan="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="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 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<h3 class="text-xl font-semibold text-gray-400" x-text="$store.global.t('noAccountsYet')">
No Accounts Yet
</h3>
<p class="text-sm text-gray-600 max-w-md leading-relaxed" x-text="$store.global.t('noAccountsDesc')">
Get started by adding a Google account via OAuth, or use the CLI command to import credentials.
</p>
<div class="flex items-center gap-4 mt-2">
<button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-sm gap-2 shadow-lg shadow-neon-purple/20"
onclick="document.getElementById('add_account_modal').showModal()">
<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('addFirstAccount')">Add Your First Account</span>
</button>
<span class="text-xs text-gray-600" x-text="$store.global.t('or')">or</span>
<div class="text-xs font-mono bg-space-800 px-3 py-2 rounded border border-space-border text-gray-400">
npm run accounts:add
</div>
</div>
</div>
</td>
</tr>
</template>
<!-- Account Rows -->
<template x-for="acc in filteredAccounts" :key="acc.email">
<tr class="border-b border-space-border/30 last:border-0 hover:bg-white/5 transition-colors group">
<td class="pl-6 py-4">
<label class="relative inline-flex items-center cursor-pointer">
@@ -54,9 +102,7 @@
<td class="py-4">
<div class="tooltip tooltip-right" :data-tip="acc.email">
<span class="font-mono text-sm text-gray-300 truncate max-w-[320px] inline-block cursor-help group-hover:text-white transition-colors"
x-text="acc.email.length > 40 ?
acc.email.substring(0, 15) + '...' + acc.email.slice(-18) :
acc.email">
x-text="formatEmail(acc.email)">
</span>
</div>
</td>
@@ -66,7 +112,6 @@
x-text="acc.source || 'oauth'">
</span>
</td>
<td class="py-4 font-mono text-xs text-gray-500" x-text="acc.projectId || '-'"></td>
<td class="py-4">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full flex-shrink-0"
@@ -81,7 +126,7 @@
</div>
</td>
<td class="py-4 pr-6">
<div class="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<div class="flex justify-end gap-2">
<!-- Fix Button -->
<button x-show="acc.status === 'invalid'"
class="px-3 py-1 text-[10px] font-bold font-mono uppercase tracking-wider rounded bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20 border border-yellow-500/30 hover:border-yellow-500/50 transition-all"
@@ -100,7 +145,7 @@
class="p-2 rounded transition-colors"
:class="acc.source === 'database' ? 'text-gray-700 cursor-not-allowed' : 'hover:bg-red-500/10 text-gray-500 hover:text-red-400'"
:disabled="acc.source === 'database'"
@click="acc.source !== 'database' && deleteAccount(acc.email)"
@click="acc.source !== 'database' && confirmDeleteAccount(acc.email)"
:title="acc.source === 'database' ? $store.global.t('cannotDeleteDatabase') : $store.global.t('delete')">
<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"
@@ -111,7 +156,71 @@
</td>
</tr>
</template>
<!-- No Search Results -->
<template x-if="$store.data.accounts.length > 0 && filteredAccounts.length === 0">
<tr>
<td colspan="6" class="py-12 text-center">
<div class="flex flex-col items-center gap-3">
<svg class="w-12 h-12 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
<p class="text-sm text-gray-600" x-text="$store.global.t('noSearchResults')">No accounts match your search</p>
<button class="btn btn-xs btn-ghost text-gray-500" @click="searchQuery = ''" x-text="$store.global.t('clearSearch')">Clear Search</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Account Status Info -->
<div x-show="$store.data.accounts.length > 0" class="mt-4 px-6 py-3 bg-space-900/20 rounded-lg border border-space-border/20">
<p class="text-xs text-gray-600 flex items-center gap-2">
<svg class="w-4 h-4 text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span x-html="$store.global.t('disabledAccountsNote')"></span>
</p>
</div>
<!-- Delete Confirmation Modal -->
<dialog id="delete_account_modal" class="modal">
<div class="modal-box bg-space-900 border-2 border-red-500/50">
<h3 class="font-bold text-lg text-red-400 flex items-center gap-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
<span x-text="$store.global.t('dangerousOperation')">Dangerous Operation</span>
</h3>
<p class="py-4 text-gray-300">
<span x-text="$store.global.t('confirmDeletePrompt')">Are you sure you want to delete account</span>
<strong class="text-white font-mono" x-text="deleteTarget"></strong>?
</p>
<div class="bg-red-500/10 border border-red-500/30 rounded p-3 mb-4">
<p class="text-sm text-red-300 flex items-start gap-2">
<svg class="w-5 h-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
<span x-text="$store.global.t('deleteWarning')">This action cannot be undone. All configuration and historical records will be permanently deleted.</span>
</p>
</div>
<div class="modal-action">
<button class="btn btn-ghost text-gray-400" onclick="document.getElementById('delete_account_modal').close()" x-text="$store.global.t('cancel')">
Cancel
</button>
<button class="btn bg-red-500 hover:bg-red-600 border-none text-white"
@click="executeDelete()">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<span x-text="$store.global.t('confirmDelete')">Confirm Delete</span>
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</div>

View File

@@ -1,10 +1,39 @@
<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 items-baseline gap-3">
<h1 class="text-2xl font-bold text-white tracking-tight" x-text="$store.global.t('dashboard')">
Dashboard
</h1>
<span class="text-[10px] font-mono text-gray-600 uppercase tracking-[0.15em]"
x-text="$store.global.t('systemDesc')">
CLAUDE PROXY SYSTEM
</span>
</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">
<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">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>
<!-- Stats Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div
class="stat bg-space-900/40 border border-space-border/30 rounded-lg p-4 hover:border-space-border/60 transition-colors group relative">
class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-6 hover:border-cyan-500/30 hover:bg-cyan-500/5 transition-all duration-300 group relative cursor-pointer"
@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-gray-600/60 transition-colors">
<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-5 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">
@@ -15,14 +44,19 @@
<div class="stat-value text-white font-mono text-4xl font-bold mb-1" x-text="stats.total"></div>
<div class="stat-title text-gray-500 font-mono text-xs uppercase tracking-wider truncate"
x-text="$store.global.t('totalAccounts')"></div>
<div class="stat-desc text-gray-600 text-[10px] truncate" x-text="$store.global.t('registeredNodes')">
Registered
Nodes</div>
<div class="stat-desc text-cyan-400/60 text-[10px] truncate flex items-center gap-1">
<span x-text="$store.global.t('registeredNodes')"></span>
<svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity" 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-lg p-4 hover:border-space-border/60 transition-colors group relative">
<div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-neon-green/50 transition-colors">
class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-6 hover:border-green-500/30 hover:bg-green-500/5 transition-all duration-300 group relative cursor-pointer"
@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-5 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>
@@ -31,13 +65,19 @@
<div class="stat-value text-white font-mono text-4xl font-bold mb-1" x-text="stats.active"></div>
<div class="stat-title text-gray-500 font-mono text-xs uppercase tracking-wider truncate"
x-text="$store.global.t('active')"></div>
<div class="stat-desc text-neon-green/60 text-[10px] truncate" x-text="$store.global.t('operational')">
<div class="stat-desc text-green-400/60 text-[10px] truncate flex items-center gap-1">
<span x-text="$store.global.t('operational')"></span>
<svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity" 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-lg p-4 hover:border-space-border/60 transition-colors group relative">
<div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-red-500/50 transition-colors">
class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-6 hover:border-red-500/30 hover:bg-red-500/5 transition-all duration-300 group relative cursor-pointer"
@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-5 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>
@@ -46,14 +86,19 @@
<div class="stat-value text-white font-mono text-4xl font-bold mb-1" x-text="stats.limited"></div>
<div class="stat-title text-gray-500 font-mono text-xs uppercase tracking-wider truncate"
x-text="$store.global.t('rateLimited')"></div>
<div class="stat-desc text-red-500/60 text-[10px] truncate" x-text="$store.global.t('cooldown')"></div>
<div class="stat-desc text-red-500/60 text-[10px] truncate flex items-center gap-1">
<span x-text="$store.global.t('cooldown')"></span>
<svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity" 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-lg p-4 col-span-1 lg:col-start-4 lg:row-start-1 h-full flex items-center justify-between gap-3 overflow-hidden relative group hover:border-space-border/60 transition-colors">
class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-6 col-span-1 lg:col-start-4 lg:row-start-1 h-full flex items-center justify-between gap-3 overflow-hidden relative group hover:border-space-border/60 transition-colors">
<!-- Chart Container -->
<div class="h-16 w-16 lg:h-20 lg:w-20 relative flex-shrink-0">
<div class="h-14 w-14 lg:h-16 lg:w-16 relative flex-shrink-0">
<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>
@@ -61,7 +106,7 @@
</div>
<!-- Legend / Info -->
<div class="flex flex-col justify-center gap-2 flex-grow min-w-0 z-10">
<div class="flex flex-col justify-center gap-2 flex-grow min-w-0">
<div class="flex items-center justify-between">
<span class="text-[10px] text-gray-500 uppercase tracking-wider font-mono truncate"
x-text="$store.global.t('globalQuota')">Global Quota</span>
@@ -69,35 +114,37 @@
<!-- Custom Legend -->
<div class="space-y-1">
<div class="flex items-center justify-between text-[10px] text-gray-400">
<div class="flex items-center justify-between text-[10px] text-gray-400 cursor-pointer hover:text-neon-purple transition-colors group/legend"
@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 truncate">
<div
class="w-1.5 h-1.5 rounded-full bg-neon-purple shadow-[0_0_4px_rgba(168,85,247,0.4)] flex-shrink-0">
</div>
<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>
<svg class="w-2.5 h-2.5 opacity-0 group-hover/legend:opacity-100 transition-opacity" 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 text-[10px] text-gray-400">
<div class="flex items-center justify-between text-[10px] text-gray-400 cursor-pointer hover:text-neon-green transition-colors group/legend"
@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 truncate">
<div
class="w-1.5 h-1.5 rounded-full bg-neon-green shadow-[0_0_4px_rgba(34,197,94,0.4)] flex-shrink-0">
</div>
<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>
<svg class="w-2.5 h-2.5 opacity-0 group-hover/legend:opacity-100 transition-opacity" 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>
<!-- Decorative Glow -->
<div class="absolute -right-6 -top-6 w-24 h-24 bg-neon-purple/5 rounded-full blur-2xl pointer-events-none">
</div>
</div>
</div>
<!-- Usage Trend Chart -->
<div class="glass-panel p-4 rounded-lg">
<div class="view-card">
<!-- Header with Stats and Filter -->
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-4">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
<div class="flex flex-wrap items-center gap-4">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
@@ -111,17 +158,17 @@
<!-- Usage Stats Pills -->
<div class="flex flex-wrap gap-2 text-[10px] font-mono">
<div class="px-2 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
<div class="px-2.5 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
<span class="text-gray-500" x-text="$store.global.t('totalColon')">Total:</span>
<span class="text-white ml-1" x-text="usageStats.total"></span>
<span class="text-white ml-1 font-bold" x-text="usageStats.total"></span>
</div>
<div class="px-2 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
<div class="px-2.5 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
<span class="text-gray-500" x-text="$store.global.t('todayColon')">Today:</span>
<span class="text-neon-cyan ml-1" x-text="usageStats.today"></span>
<span class="text-neon-cyan ml-1 font-bold" x-text="usageStats.today"></span>
</div>
<div class="px-2 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
<div class="px-2.5 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
<span class="text-gray-500" x-text="$store.global.t('hour1Colon')">1H:</span>
<span class="text-neon-green ml-1" x-text="usageStats.thisHour"></span>
<span class="text-neon-green ml-1 font-bold" x-text="usageStats.thisHour"></span>
</div>
</div>
</div>

View File

@@ -62,24 +62,33 @@
</div>
<!-- Log Content -->
<div id="logs-container" class="flex-1 overflow-auto p-4 font-mono text-xs space-y-0.5 bg-space-950">
<div id="logs-container" class="flex-1 overflow-auto p-4 font-mono text-[11px] leading-relaxed bg-space-950 custom-scrollbar">
<template x-for="(log, idx) in filteredLogs" :key="idx">
<div class="hover:bg-white/5 rounded px-2 py-0.5 -mx-2 break-words leading-tight flex gap-3 group">
<span class="text-gray-600 w-16 shrink-0 select-none group-hover:text-gray-500"
<div class="flex gap-4 px-2 py-0.5 -mx-2 hover:bg-white/[0.03] transition-colors group">
<!-- Timestamp: Muted & Fixed Width -->
<span class="text-zinc-600 w-16 shrink-0 select-none group-hover:text-zinc-500 transition-colors"
x-text="new Date(log.timestamp).toLocaleTimeString([], {hour12:false})"></span>
<span class="font-bold w-12 shrink-0 select-none" :class="{
'text-blue-400': log.level === 'INFO',
'text-yellow-400': log.level === 'WARN',
'text-red-500': log.level === 'ERROR',
'text-neon-green': log.level === 'SUCCESS',
'text-purple-400': log.level === 'DEBUG'
}" x-text="log.level"></span>
<span class="text-gray-300 break-all" x-html="log.message.replace(/\n/g, '<br>')"></span>
<!-- Level: Tag Style -->
<div class="w-14 shrink-0 flex items-center">
<span class="px-1.5 py-0.5 rounded-[2px] text-[10px] font-bold uppercase tracking-wider leading-none border"
:class="{
'bg-blue-500/10 text-blue-400 border-blue-500/20': log.level === 'INFO',
'bg-yellow-500/10 text-yellow-400 border-yellow-500/20': log.level === 'WARN',
'bg-red-500/10 text-red-500 border-red-500/20': log.level === 'ERROR',
'bg-emerald-500/10 text-emerald-400 border-emerald-500/20': log.level === 'SUCCESS',
'bg-purple-500/10 text-purple-400 border-purple-500/20': log.level === 'DEBUG'
}" x-text="log.level"></span>
</div>
<!-- Message: Clean & High Contrast -->
<span class="text-zinc-300 break-all group-hover:text-white transition-colors flex-1"
x-html="log.message.replace(/\n/g, '<br>')"></span>
</div>
</template>
<!-- Blinking Cursor -->
<div class="h-4 w-2 bg-gray-500 animate-pulse mt-1 inline-block" x-show="filteredLogs.length === logs.length && !searchQuery"></div>
<div x-show="filteredLogs.length === 0 && logs.length > 0" class="text-gray-600 italic mt-4 text-center"
<div class="h-3 w-1.5 bg-zinc-600 animate-pulse mt-1 inline-block" x-show="filteredLogs.length === logs.length && !searchQuery"></div>
<div x-show="filteredLogs.length === 0 && logs.length > 0" class="text-zinc-700 italic mt-8 text-center"
x-text="$store.global.t('noLogsMatch')">
No logs match filter
</div>

View File

@@ -1,20 +1,48 @@
<div x-data="models" class="view-container">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-white" x-text="$store.global.t('models')">Models</h2>
<p class="text-sm text-gray-500" x-text="$store.global.t('modelsPageDesc')">Real-time quota and status for
all available models.</p>
<!-- Compact Header -->
<div class="flex items-center justify-between gap-4 mb-6">
<!-- Title with inline subtitle -->
<div class="flex items-baseline gap-3">
<h1 class="text-2xl font-bold text-white tracking-tight" x-text="$store.global.t('models')">
Models
</h1>
<span class="text-[10px] font-mono text-gray-600 uppercase tracking-[0.15em]"
x-text="$store.global.t('modelsPageDesc')">
Real-time quota and status for all available models.
</span>
</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')"
class="w-full h-full bg-space-800 border border-space-border text-gray-300 rounded-lg pl-10 pr-10 focus:outline-none focus:border-neon-purple focus:ring-1 focus:ring-neon-purple transition-all text-xs placeholder-gray-600"
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="flex flex-col lg:flex-row items-center justify-between gap-4 glass-panel p-4 rounded-lg h-auto mb-6">
<div class="flex flex-col md:flex-row items-center gap-4 w-full lg:w-auto">
<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-10">
<div class="relative w-full md:w-64 h-9">
<select
class="appearance-none 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-sm"
class="appearance-none 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"
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">
@@ -29,43 +57,37 @@
</div>
<!-- Filter Buttons -->
<div class="join h-10 w-full md:w-auto overflow-x-auto">
<div class="join h-9 w-full md:w-auto overflow-x-auto">
<button
class="join-item btn btn-sm h-full flex-1 md:flex-none px-4 md:px-6 border-space-border bg-space-800 text-gray-400 hover:text-white hover:bg-space-700 hover:border-space-600 transition-all font-medium text-xs tracking-wide whitespace-nowrap"
:class="{'bg-neon-purple text-white border-neon-purple hover:bg-purple-600 hover:border-purple-500': $store.data.filters.family === 'all'}"
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-sm h-full flex-1 md:flex-none px-4 md:px-6 border-space-border bg-space-800 text-gray-400 hover:text-white hover:bg-space-700 hover:border-space-600 transition-all font-medium text-xs tracking-wide whitespace-nowrap"
:class="{'bg-neon-purple text-white border-neon-purple hover:bg-purple-600 hover:border-purple-500': $store.data.filters.family === 'claude'}"
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-sm h-full flex-1 md:flex-none px-4 md:px-6 border-space-border bg-space-800 text-gray-400 hover:text-white hover:bg-space-700 hover:border-space-600 transition-all font-medium text-xs tracking-wide whitespace-nowrap"
:class="{'bg-neon-purple text-white border-neon-purple hover:bg-purple-600 hover:border-purple-500': $store.data.filters.family === 'gemini'}"
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>
</div>
<!-- Search -->
<div class="relative w-full md:w-72 h-10">
<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')"
class="w-full h-full bg-space-800 border border-space-border text-gray-300 rounded-lg pl-10 pr-4 focus:outline-none focus:border-neon-purple focus:ring-1 focus:ring-neon-purple transition-all text-sm placeholder-gray-600"
x-model.debounce="$store.data.filters.search" @input="$store.data.computeQuotaRows()">
</div>
</div>
<!-- Main Table -->
<div class="glass-panel rounded-xl overflow-x-auto min-h-[400px]">
<table class="standard-table"
:class="{'table-xs': $store.settings.compact, 'table-sm': !$store.settings.compact}">
<!-- 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 py-3 pl-4 whitespace-nowrap" x-text="$store.global.t('stat')">Stat</th>
@@ -100,6 +122,9 @@
<div class="flex justify-between text-xs font-mono">
<span x-text="row.avgQuota + '%'"
:class="row.avgQuota > 0 ? 'text-white' : 'text-red-500'"></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>
</div>
<progress class="progress w-full h-1 bg-space-800"
:class="row.avgQuota > 50 ? 'progress-gradient-success' : (row.avgQuota > 0 ? 'progress-gradient-warning' : 'progress-gradient-error')"
@@ -107,8 +132,21 @@
</div>
</td>
<td class="font-mono text-xs">
<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 class="tooltip tooltip-left"
x-data="{
get tooltipText() {
if (!row.quotaInfo || row.quotaInfo.length === 0) return 'No reset data';
const resets = row.quotaInfo
.filter(q => q.resetTime)
.map(q => `${q.email}: ${q.resetTime}`)
.join('&#10;');
return resets || 'No reset scheduled';
}
}"
:data-tip="tooltipText">
<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>
<div class="flex items-center justify-end gap-3">
@@ -121,7 +159,7 @@
<div class="flex flex-wrap gap-1 justify-end max-w-[200px]">
<template x-for="q in row.quotaInfo" :key="q.fullEmail">
<div class="tooltip tooltip-left" :data-tip="q.fullEmail + ' (' + q.pct + '%)">
<div class="w-1.5 h-3 rounded-[1px] transition-all hover:scale-125 cursor-help"
<div class="w-3 h-3 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>
@@ -136,13 +174,18 @@
<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 })"
:title="$store.global.t('pinToTop')">
@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">
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">
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>
@@ -151,16 +194,21 @@
<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 })"
:title="$store.global.t('toggleVisibility')">
@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">
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">
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>
@@ -190,4 +238,5 @@
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -73,12 +73,16 @@
</label>
<div class="join w-full grid grid-cols-2">
<button
class="join-item btn btn-sm border-space-border bg-space-800 text-gray-400 hover:text-white hover:bg-space-700 hover:border-space-600 transition-all font-medium"
:class="{'bg-neon-purple text-white border-neon-purple hover:bg-purple-600 hover:border-purple-500': $store.global.lang === 'en'}"
class="join-item btn btn-sm border-space-border/50 bg-space-800 transition-all font-medium"
:class="$store.global.lang === 'en'
? '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.global.setLang('en')">English</button>
<button
class="join-item btn btn-sm border-space-border bg-space-800 text-gray-400 hover:text-white hover:bg-space-700 hover:border-space-600 transition-all font-medium"
:class="{'bg-neon-purple text-white border-neon-purple hover:bg-purple-600 hover:border-purple-500': $store.global.lang === 'zh'}"
class="join-item btn btn-sm border-space-border/50 bg-space-800 transition-all font-medium"
:class="$store.global.lang === 'zh'
? '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.global.setLang('zh')">中文</button>
</div>
</div>
@@ -91,10 +95,16 @@
<span class="label-text-alt font-mono text-neon-purple"
x-text="$store.settings.refreshInterval + 's'"></span>
</label>
<input type="range" min="10" max="300" class="custom-range custom-range-purple"
x-model="$store.settings.refreshInterval"
:style="`background-size: ${($store.settings.refreshInterval - 10) / 2.9}% 100%`"
@change="$store.settings.saveSettings(true)">
<div class="flex gap-3 items-center">
<input type="range" min="10" max="300" class="custom-range custom-range-purple flex-1"
x-model.number="$store.settings.refreshInterval"
:style="`background-size: ${($store.settings.refreshInterval - 10) / 2.9}% 100%`"
@change="$store.settings.saveSettings(true)">
<input type="number" min="10" max="300"
class="input input-sm input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
x-model.number="$store.settings.refreshInterval"
@change="$store.settings.saveSettings(true)">
</div>
<div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono">
<span>10s</span>
<span>300s</span>
@@ -109,10 +119,16 @@
<span class="label-text-alt font-mono text-neon-purple"
x-text="$store.settings.logLimit + ' ' + $store.global.t('lines')"></span>
</label>
<input type="range" min="500" max="5000" step="500" class="custom-range custom-range-purple"
x-model="$store.settings.logLimit"
:style="`background-size: ${($store.settings.logLimit - 500) / 45}% 100%`"
@change="$store.settings.saveSettings(true)">
<div class="flex gap-3 items-center">
<input type="range" min="500" max="5000" step="500" class="custom-range custom-range-purple flex-1"
x-model.number="$store.settings.logLimit"
:style="`background-size: ${($store.settings.logLimit - 500) / 45}% 100%`"
@change="$store.settings.saveSettings(true)">
<input type="number" min="500" max="5000" step="500"
class="input input-sm input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center"
x-model.number="$store.settings.logLimit"
@change="$store.settings.saveSettings(true)">
</div>
<div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono">
<span>500</span>
<span>5000</span>
@@ -406,15 +422,14 @@
<!-- Tab 3: Models Configuration -->
<div x-show="activeTab === 'models'" x-data="window.Components.modelManager()"
class="space-y-6 max-w-3xl animate-fade-in">
<div class="flex items-center justify-between">
<div>
<div class="text-sm text-gray-400" x-text="$store.global.t('modelsDesc')">Manage visibility and
ordering of models in the dashboard.</div>
<div class="text-xs text-gray-600 mt-1" x-text="$store.global.t('modelMappingHint')"></div>
<div class="text-sm text-gray-400" x-text="$store.global.t('modelsDesc')">Configure model visibility, pinning, and request mapping.</div>
<div class="text-xs text-gray-600 mt-1" x-text="$store.global.t('modelMappingHint')">Model mapping: server-side redirection. Claude Code users: see 'Claude CLI' tab for client-side setup.</div>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500" x-text="$store.global.t('showHidden')">Show Hidden
Models</span>
<span class="text-xs text-gray-500" x-text="$store.global.t('showHidden')">Show Hidden Models</span>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer"
:checked="$store.settings.showHiddenModels === true"
@@ -431,17 +446,17 @@
<table class="standard-table">
<thead>
<tr>
<th class="pl-4 w-1/2" x-text="$store.global.t('modelId')">Model ID</th>
<th class="w-1/3" x-text="$store.global.t('modelMapping')">Mapping (Target Model ID)
</th>
<th class="text-right pr-4 w-24" x-text="$store.global.t('actions')">Actions</th>
<th class="pl-4 w-5/12" x-text="$store.global.t('modelId')">Model ID</th>
<th class="w-5/12" x-text="$store.global.t('modelMapping')">Mapping (Target Model ID)</th>
<th class="w-2/12 text-right pr-4" x-text="$store.global.t('actions')">Actions</th>
</tr>
</thead>
<tbody>
<template x-for="modelId in $store.data.models" :key="modelId">
<tr class="hover:bg-white/5 transition-colors group"
:class="isHidden ? 'opacity-50' : ''"
x-show="!isHidden || $store.settings.showHiddenModels" x-data="{
x-show="!isHidden || $store.settings.showHiddenModels"
x-data="{
newMapping: '',
get config() { return $store.data.modelConfig[modelId] || {} },
get isPinned() { return !!this.config.pinned },
@@ -451,13 +466,20 @@
return (family === 'other' || family === 'unknown');
}
}" x-init="newMapping = config.mapping || ''">
<td class="pl-4 font-mono text-xs text-gray-300" x-text="modelId"></td>
<td class="pl-4 font-mono text-xs text-gray-300">
<div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full"
:class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
<span x-text="modelId"></span>
</div>
</td>
<td>
<div x-show="!isEditing(modelId)"
class="flex items-center gap-2 group-hover:text-white transition-colors cursor-pointer"
@click="startEditing(modelId); newMapping = config.mapping || ''">
<span x-text="config.mapping || '-'"
:class="{'text-gray-600 italic': !config.mapping}"></span>
class="flex items-center gap-2 group-hover:text-white transition-colors cursor-pointer py-2"
@click="startEditing(modelId); newMapping = config.mapping || ''; $nextTick(() => $refs['input-' + modelId]?.focus())">
<span x-text="config.mapping || 'Click to set...'"
:class="{'text-gray-600 italic': !config.mapping, 'text-gray-300': config.mapping}"
class="text-xs font-mono"></span>
<svg class="w-3 h-3 text-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -466,24 +488,39 @@
</div>
<div x-show="isEditing(modelId)" class="flex items-center gap-1">
<input type="text" x-model="newMapping"
class="input input-xs bg-space-800 border-space-border text-white focus:outline-none focus:border-neon-cyan w-40"
placeholder="e.g. claude-sonnet-4-5"
:x-ref="'input-' + modelId"
class="input input-xs bg-space-800 border-space-border text-white focus:outline-none focus:border-neon-cyan flex-1 font-mono text-xs"
placeholder="e.g. claude-sonnet-4-5 or gemini-3-flash"
@keydown.enter="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()"
@keydown.escape="newMapping = config.mapping || ''; stopEditing()">
<button class="btn btn-xs btn-ghost btn-square text-green-500"
@click="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()"><svg
xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
<button class="btn btn-xs btn-ghost btn-square text-green-500 hover:bg-green-500/20"
@click="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()"
title="Save">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M5 13l4 4L19 7" />
</svg></button>
<button class="btn btn-xs btn-ghost btn-square text-gray-500"
@click="newMapping = config.mapping || ''; stopEditing()"><svg
xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
</svg>
</button>
<button class="btn btn-xs btn-ghost btn-square text-gray-500 hover:bg-gray-500/20"
@click="newMapping = config.mapping || ''; stopEditing()"
title="Cancel">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" 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>
</svg>
</button>
<button x-show="config.mapping"
class="btn btn-xs btn-ghost btn-square text-red-400 hover:bg-red-500/20"
@click="await updateModelConfig(modelId, { mapping: '' }); stopEditing()"
title="Clear mapping">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
<td class="text-right pr-4">
@@ -533,7 +570,7 @@
</tr>
</template>
<tr x-show="!$store.data.models.length">
<td colspan="4" class="text-center py-8 text-gray-600 text-xs font-mono"
<td colspan="3" class="text-center py-8 text-gray-600 text-xs font-mono"
x-text="$store.global.t('noModels')">
NO MODELS DETECTED
</td>
@@ -675,10 +712,16 @@
<span class="label-text-alt font-mono text-neon-purple text-xs font-semibold"
x-text="serverConfig.maxRetries || 5"></span>
</label>
<input type="range" min="1" max="20" class="custom-range custom-range-purple"
:value="serverConfig.maxRetries || 5"
:style="`background-size: ${((serverConfig.maxRetries || 5) - 1) / 19 * 100}% 100%`"
@input="toggleMaxRetries($event.target.value)">
<div class="flex gap-3 items-center">
<input type="range" min="1" max="20" class="custom-range custom-range-purple flex-1"
:value="serverConfig.maxRetries || 5"
:style="`background-size: ${((serverConfig.maxRetries || 5) - 1) / 19 * 100}% 100%`"
@input="toggleMaxRetries($event.target.value)">
<input type="number" min="1" max="20"
class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.maxRetries || 5"
@change="toggleMaxRetries($event.target.value)">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
@@ -689,11 +732,17 @@
<span class="label-text-alt font-mono text-neon-green text-xs font-semibold"
x-text="((serverConfig.retryBaseMs || 1000) < 10000 ? (serverConfig.retryBaseMs || 1000) + 'ms' : Math.round((serverConfig.retryBaseMs || 1000) / 1000) + 's')"></span>
</label>
<input type="range" min="100" max="10000" step="100"
class="custom-range custom-range-green"
:value="serverConfig.retryBaseMs || 1000"
:style="`background-size: ${((serverConfig.retryBaseMs || 1000) - 100) / 99}% 100%`"
@input="toggleRetryBaseMs($event.target.value)">
<div class="flex gap-2 items-center">
<input type="range" min="100" max="10000" step="100"
class="custom-range custom-range-green flex-1"
:value="serverConfig.retryBaseMs || 1000"
:style="`background-size: ${((serverConfig.retryBaseMs || 1000) - 100) / 99}% 100%`"
@input="toggleRetryBaseMs($event.target.value)">
<input type="number" min="100" max="10000" step="100"
class="input input-xs input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.retryBaseMs || 1000"
@change="toggleRetryBaseMs($event.target.value)">
</div>
</div>
<div class="form-control">
<label class="label pt-0">
@@ -702,11 +751,17 @@
<span class="label-text-alt font-mono text-neon-green text-xs font-semibold"
x-text="Math.round((serverConfig.retryMaxMs || 30000) / 1000) + 's'"></span>
</label>
<input type="range" min="1000" max="120000" step="1000"
class="custom-range custom-range-green"
:value="serverConfig.retryMaxMs || 30000"
:style="`background-size: ${((serverConfig.retryMaxMs || 30000) - 1000) / 1190}% 100%`"
@input="toggleRetryMaxMs($event.target.value)">
<div class="flex gap-2 items-center">
<input type="range" min="1000" max="120000" step="1000"
class="custom-range custom-range-green flex-1"
:value="serverConfig.retryMaxMs || 30000"
:style="`background-size: ${((serverConfig.retryMaxMs || 30000) - 1000) / 1190}% 100%`"
@input="toggleRetryMaxMs($event.target.value)">
<input type="number" min="1000" max="120000" step="1000"
class="input input-xs input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.retryMaxMs || 30000"
@change="toggleRetryMaxMs($event.target.value)">
</div>
</div>
</div>
</div>
@@ -725,11 +780,17 @@
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
x-text="Math.round((serverConfig.defaultCooldownMs || 60000) / 1000) + 's'"></span>
</label>
<input type="range" min="1000" max="300000" step="1000"
class="custom-range custom-range-cyan"
:value="serverConfig.defaultCooldownMs || 60000"
:style="`background-size: ${((serverConfig.defaultCooldownMs || 60000) - 1000) / 2990}% 100%`"
@input="toggleDefaultCooldownMs($event.target.value)">
<div class="flex gap-3 items-center">
<input type="range" min="1000" max="300000" step="1000"
class="custom-range custom-range-cyan flex-1"
:value="serverConfig.defaultCooldownMs || 60000"
:style="`background-size: ${((serverConfig.defaultCooldownMs || 60000) - 1000) / 2990}% 100%`"
@input="toggleDefaultCooldownMs($event.target.value)">
<input type="number" min="1000" max="300000" step="1000"
class="input input-xs input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.defaultCooldownMs || 60000"
@change="toggleDefaultCooldownMs($event.target.value)">
</div>
</div>
<div class="form-control">
@@ -739,11 +800,17 @@
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
x-text="((serverConfig.maxWaitBeforeErrorMs || 120000) >= 60000 ? Math.round((serverConfig.maxWaitBeforeErrorMs || 120000) / 60000) + 'm' : Math.round((serverConfig.maxWaitBeforeErrorMs || 120000) / 1000) + 's')"></span>
</label>
<input type="range" min="0" max="600000" step="10000"
class="custom-range custom-range-cyan"
:value="serverConfig.maxWaitBeforeErrorMs || 120000"
:style="`background-size: ${(serverConfig.maxWaitBeforeErrorMs || 120000) / 6000}% 100%`"
@input="toggleMaxWaitBeforeErrorMs($event.target.value)">
<div class="flex gap-3 items-center">
<input type="range" min="0" max="600000" step="10000"
class="custom-range custom-range-cyan flex-1"
:value="serverConfig.maxWaitBeforeErrorMs || 120000"
:style="`background-size: ${(serverConfig.maxWaitBeforeErrorMs || 120000) / 6000}% 100%`"
@input="toggleMaxWaitBeforeErrorMs($event.target.value)">
<input type="number" min="0" max="600000" step="10000"
class="input input-xs input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.maxWaitBeforeErrorMs || 120000"
@change="toggleMaxWaitBeforeErrorMs($event.target.value)">
</div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('maxWaitDesc')">Maximum time to wait for a sticky account to
reset before switching.</p>