style(webui): refine UI polish and enhance component interactions
This commit is contained in:
@@ -114,16 +114,47 @@ document.addEventListener('alpine:init', () => {
|
|||||||
Alpine.store('global').showToast(Alpine.store('global').t('oauthInProgress'), 'info');
|
Alpine.store('global').showToast(Alpine.store('global').t('oauthInProgress'), 'info');
|
||||||
|
|
||||||
// Open OAuth window
|
// Open OAuth window
|
||||||
window.open(data.url, 'google_oauth', 'width=600,height=700,scrollbars=yes');
|
const oauthWindow = window.open(data.url, 'google_oauth', 'width=600,height=700,scrollbars=yes');
|
||||||
|
|
||||||
// Poll for account changes instead of relying on postMessage
|
// Poll for account changes instead of relying on postMessage
|
||||||
// (since OAuth callback is now on port 51121, not this server)
|
// (since OAuth callback is now on port 51121, not this server)
|
||||||
const initialAccountCount = Alpine.store('data').accounts.length;
|
const initialAccountCount = Alpine.store('data').accounts.length;
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
const maxPolls = 60; // 2 minutes (2 second intervals)
|
const maxPolls = 60; // 2 minutes (2 second intervals)
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
// Show progress modal
|
||||||
|
Alpine.store('global').oauthProgress = {
|
||||||
|
active: true,
|
||||||
|
current: 0,
|
||||||
|
max: maxPolls,
|
||||||
|
cancel: () => {
|
||||||
|
cancelled = true;
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
Alpine.store('global').oauthProgress.active = false;
|
||||||
|
Alpine.store('global').showToast(Alpine.store('global').t('oauthCancelled'), 'info');
|
||||||
|
if (oauthWindow && !oauthWindow.closed) {
|
||||||
|
oauthWindow.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pollInterval = setInterval(async () => {
|
const pollInterval = setInterval(async () => {
|
||||||
|
if (cancelled) {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pollCount++;
|
pollCount++;
|
||||||
|
Alpine.store('global').oauthProgress.current = pollCount;
|
||||||
|
|
||||||
|
// Check if OAuth window was closed manually
|
||||||
|
if (oauthWindow && oauthWindow.closed && !cancelled) {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
Alpine.store('global').oauthProgress.active = false;
|
||||||
|
Alpine.store('global').showToast(Alpine.store('global').t('oauthWindowClosed'), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh account list
|
// Refresh account list
|
||||||
await Alpine.store('data').fetchData();
|
await Alpine.store('data').fetchData();
|
||||||
@@ -132,6 +163,8 @@ document.addEventListener('alpine:init', () => {
|
|||||||
const currentAccountCount = Alpine.store('data').accounts.length;
|
const currentAccountCount = Alpine.store('data').accounts.length;
|
||||||
if (currentAccountCount > initialAccountCount) {
|
if (currentAccountCount > initialAccountCount) {
|
||||||
clearInterval(pollInterval);
|
clearInterval(pollInterval);
|
||||||
|
Alpine.store('global').oauthProgress.active = false;
|
||||||
|
|
||||||
const actionKey = reAuthEmail ? 'reauthenticated' : 'added';
|
const actionKey = reAuthEmail ? 'reauthenticated' : 'added';
|
||||||
const action = Alpine.store('global').t(actionKey);
|
const action = Alpine.store('global').t(actionKey);
|
||||||
const successfully = Alpine.store('global').t('successfully');
|
const successfully = Alpine.store('global').t('successfully');
|
||||||
@@ -140,11 +173,20 @@ document.addEventListener('alpine:init', () => {
|
|||||||
'success'
|
'success'
|
||||||
);
|
);
|
||||||
document.getElementById('add_account_modal')?.close();
|
document.getElementById('add_account_modal')?.close();
|
||||||
|
|
||||||
|
if (oauthWindow && !oauthWindow.closed) {
|
||||||
|
oauthWindow.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop polling after max attempts
|
// Stop polling after max attempts
|
||||||
if (pollCount >= maxPolls) {
|
if (pollCount >= maxPolls) {
|
||||||
clearInterval(pollInterval);
|
clearInterval(pollInterval);
|
||||||
|
Alpine.store('global').oauthProgress.active = false;
|
||||||
|
Alpine.store('global').showToast(
|
||||||
|
Alpine.store('global').t('oauthTimeout'),
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, 2000); // Poll every 2 seconds
|
}, 2000); // Poll every 2 seconds
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
:root {
|
:root {
|
||||||
|
/* === Background Layers === */
|
||||||
--color-space-950: #09090b;
|
--color-space-950: #09090b;
|
||||||
--color-space-900: #0f0f11;
|
--color-space-900: #0f0f11;
|
||||||
--color-space-850: #121214;
|
--color-space-850: #121214;
|
||||||
--color-space-800: #18181b;
|
--color-space-800: #18181b;
|
||||||
--color-space-border: #27272a;
|
--color-space-border: #27272a;
|
||||||
|
|
||||||
|
/* === Neon Accents (Full Saturation) === */
|
||||||
--color-neon-purple: #a855f7;
|
--color-neon-purple: #a855f7;
|
||||||
--color-neon-green: #22c55e;
|
--color-neon-green: #22c55e;
|
||||||
--color-neon-cyan: #06b6d4;
|
--color-neon-cyan: #06b6d4;
|
||||||
--color-neon-yellow: #eab308;
|
--color-neon-yellow: #eab308;
|
||||||
--color-neon-red: #ef4444;
|
--color-neon-red: #ef4444;
|
||||||
--color-text-main: #d1d5db; /* gray-300 */
|
|
||||||
--color-text-dim: #71717a; /* zinc-400 */
|
/* === Soft Neon (Reduced Saturation for Fills) === */
|
||||||
--color-text-muted: #6b7280; /* gray-500 */
|
--color-neon-purple-soft: #9333ea;
|
||||||
--color-text-bright: #ffffff;
|
--color-neon-green-soft: #16a34a;
|
||||||
|
--color-neon-cyan-soft: #0891b2;
|
||||||
|
|
||||||
|
/* === Text Hierarchy (WCAG AA Compliant) === */
|
||||||
|
--color-text-primary: #ffffff; /* Emphasis: Titles, Key Numbers */
|
||||||
|
--color-text-secondary: #d4d4d8; /* Content: Body Text (zinc-300) */
|
||||||
|
--color-text-tertiary: #a1a1aa; /* Metadata: Timestamps, Labels (zinc-400) */
|
||||||
|
--color-text-quaternary: #71717a; /* Subtle: Decorative (zinc-500) */
|
||||||
|
|
||||||
|
/* === Legacy Aliases (Backward Compatibility) === */
|
||||||
|
--color-text-main: var(--color-text-secondary);
|
||||||
|
--color-text-dim: var(--color-text-tertiary);
|
||||||
|
--color-text-muted: var(--color-text-tertiary);
|
||||||
|
--color-text-bright: var(--color-text-primary);
|
||||||
|
|
||||||
/* Gradient Accents */
|
/* Gradient Accents */
|
||||||
--color-green-400: #4ade80;
|
--color-green-400: #4ade80;
|
||||||
@@ -44,20 +60,25 @@
|
|||||||
|
|
||||||
/* Custom Scrollbar */
|
/* Custom Scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 8px;
|
||||||
height: 6px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: rgba(9, 9, 11, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@apply bg-space-800 rounded-full;
|
background: linear-gradient(180deg, #27272a 0%, #18181b 100%);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
@apply bg-space-border;
|
background: linear-gradient(180deg, #3f3f46 0%, #27272a 100%);
|
||||||
|
border-color: rgba(168, 85, 247, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@@ -82,9 +103,23 @@
|
|||||||
|
|
||||||
/* Utility */
|
/* Utility */
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
background: theme('colors.space.900 / 70%');
|
background: linear-gradient(135deg,
|
||||||
|
rgba(15, 15, 17, 0.75) 0%,
|
||||||
|
rgba(18, 18, 20, 0.70) 100%
|
||||||
|
);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
border: 1px solid theme('colors.white / 8%');
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.02) inset,
|
||||||
|
0 4px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-panel:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.04) inset,
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
@@ -128,23 +163,77 @@
|
|||||||
/* Refactored Global Utilities */
|
/* Refactored Global Utilities */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* View Containers */
|
/* Standard Layout Constants */
|
||||||
.view-container {
|
:root {
|
||||||
@apply mx-auto p-6 space-y-6 animate-fade-in;
|
--view-padding: 2rem; /* 32px - Standard Padding */
|
||||||
/* Responsive max-width: use most of screen on small displays,
|
--view-gap: 1.5rem; /* 24px - Standard component gap */
|
||||||
but cap at 1600px on large displays for reading comfort */
|
--card-radius: 0.75rem; /* 12px */
|
||||||
max-width: min(95%, 1600px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section Headers */
|
@media (max-width: 768px) {
|
||||||
.section-header {
|
:root {
|
||||||
@apply flex justify-between items-center mb-6;
|
--view-padding: 1rem;
|
||||||
|
--view-gap: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.section-title {
|
|
||||||
|
/* Base View Container */
|
||||||
|
.view-container {
|
||||||
|
@apply mx-auto w-full animate-fade-in flex flex-col;
|
||||||
|
padding: var(--view-padding);
|
||||||
|
gap: var(--view-gap);
|
||||||
|
min-height: calc(100vh - 56px); /* Align with navbar height */
|
||||||
|
max-width: 1400px;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specialized container for data-heavy pages (Logs) */
|
||||||
|
.view-container-full {
|
||||||
|
@apply w-full animate-fade-in flex flex-col;
|
||||||
|
padding: var(--view-padding);
|
||||||
|
gap: var(--view-gap);
|
||||||
|
min-height: calc(100vh - 56px);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Centered container for form-heavy pages (Settings/Accounts) */
|
||||||
|
.view-container-centered {
|
||||||
|
@apply mx-auto w-full animate-fade-in flex flex-col;
|
||||||
|
padding: var(--view-padding);
|
||||||
|
gap: var(--view-gap);
|
||||||
|
min-height: calc(100vh - 56px);
|
||||||
|
max-width: 900px; /* Comfortable reading width for forms */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Standard Section Header */
|
||||||
|
.view-header {
|
||||||
|
@apply flex flex-col md:flex-row md:items-end justify-between mb-2;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-header-title {
|
||||||
|
@apply flex flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-header-title h2 {
|
||||||
@apply text-2xl font-bold text-white tracking-tight;
|
@apply text-2xl font-bold text-white tracking-tight;
|
||||||
}
|
}
|
||||||
.section-desc {
|
|
||||||
@apply text-gray-500 text-sm;
|
.view-header-title p {
|
||||||
|
@apply text-sm text-gray-500 mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-header-actions {
|
||||||
|
@apply flex items-center gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Standard Card Panel */
|
||||||
|
.view-card {
|
||||||
|
@apply glass-panel rounded-xl p-6 border border-space-border/50 relative overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-card-header {
|
||||||
|
@apply flex items-center justify-between mb-4 pb-4 border-b border-space-border/30;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Component Unification */
|
/* Component Unification */
|
||||||
@@ -155,77 +244,73 @@
|
|||||||
@apply bg-space-900/50 text-gray-500 font-mono text-xs uppercase border-b border-space-border;
|
@apply bg-space-900/50 text-gray-500 font-mono text-xs uppercase border-b border-space-border;
|
||||||
}
|
}
|
||||||
.standard-table tbody tr {
|
.standard-table tbody tr {
|
||||||
@apply hover:bg-white/5 transition-colors border-b border-space-border/30 last:border-0;
|
@apply transition-all duration-200 border-b border-space-border/30 last:border-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Range Slider */
|
.standard-table tbody tr:hover {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(255, 255, 255, 0.03) 0%,
|
||||||
|
rgba(255, 255, 255, 0.05) 50%,
|
||||||
|
rgba(255, 255, 255, 0.03) 100%
|
||||||
|
);
|
||||||
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Range Slider - Simplified */
|
||||||
.custom-range {
|
.custom-range {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 6px;
|
height: 4px;
|
||||||
background: var(--color-space-800);
|
background: var(--color-space-800);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
|
||||||
background-image: linear-gradient(to right, var(--range-color) 0%, var(--range-color) 100%);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 0% 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-range::-webkit-slider-thumb {
|
.custom-range::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 18px;
|
width: 14px;
|
||||||
height: 18px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #ffffff;
|
background: var(--range-color, var(--color-neon-purple));
|
||||||
box-shadow: 0 0 10px var(--range-color-glow);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: -6px;
|
transition: transform 0.1s ease;
|
||||||
transition: transform 0.1s ease, box-shadow 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-range::-webkit-slider-thumb:hover {
|
.custom-range::-webkit-slider-thumb:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.15);
|
||||||
box-shadow: 0 0 15px var(--range-color-glow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-range::-moz-range-thumb {
|
.custom-range::-moz-range-thumb {
|
||||||
width: 18px;
|
width: 14px;
|
||||||
height: 18px;
|
height: 14px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #ffffff;
|
background: var(--range-color, var(--color-neon-purple));
|
||||||
box-shadow: 0 0 10px var(--range-color-glow);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.1s ease, box-shadow 0.2s ease;
|
transition: transform 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-range::-moz-range-thumb:hover {
|
.custom-range::-moz-range-thumb:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.15);
|
||||||
box-shadow: 0 0 15px var(--range-color-glow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color Variants */
|
/* Color Variants */
|
||||||
.custom-range-purple {
|
.custom-range-purple {
|
||||||
--range-color: var(--color-neon-purple);
|
--range-color: var(--color-neon-purple);
|
||||||
--range-color-glow: rgba(168, 85, 247, 0.5);
|
|
||||||
}
|
}
|
||||||
.custom-range-green {
|
.custom-range-green {
|
||||||
--range-color: var(--color-neon-green);
|
--range-color: var(--color-neon-green);
|
||||||
--range-color-glow: rgba(34, 197, 94, 0.5);
|
|
||||||
}
|
}
|
||||||
.custom-range-cyan {
|
.custom-range-cyan {
|
||||||
--range-color: var(--color-neon-cyan);
|
--range-color: var(--color-neon-cyan);
|
||||||
--range-color-glow: rgba(6, 182, 212, 0.5);
|
|
||||||
}
|
}
|
||||||
.custom-range-yellow {
|
.custom-range-yellow {
|
||||||
--range-color: var(--color-neon-yellow);
|
--range-color: var(--color-neon-yellow);
|
||||||
--range-color-glow: rgba(234, 179, 8, 0.5);
|
|
||||||
}
|
}
|
||||||
.custom-range-accent {
|
.custom-range-accent {
|
||||||
--range-color: var(--color-neon-cyan); /* Default accent to cyan if needed, or match DaisyUI */
|
--range-color: var(--color-neon-cyan);
|
||||||
--range-color-glow: rgba(6, 182, 212, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,32 +225,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="flex-1 overflow-auto bg-space-950 relative">
|
<div class="flex-1 overflow-auto bg-space-950 relative custom-scrollbar" style="scrollbar-gutter: stable;">
|
||||||
|
|
||||||
<!-- Views Container -->
|
<!-- Views Container -->
|
||||||
<!-- Dashboard -->
|
<!-- Dashboard -->
|
||||||
<div x-show="$store.global.activeTab === 'dashboard'" x-load-view="'dashboard'"
|
<div x-show="$store.global.activeTab === 'dashboard'" x-load-view="'dashboard'"
|
||||||
x-transition:enter="fade-enter-active" x-transition:enter-start="fade-enter-from"
|
x-transition:enter="fade-enter-active" x-transition:enter-start="fade-enter-from"
|
||||||
class="max-w-7xl mx-auto h-full"></div>
|
class="w-full"></div>
|
||||||
|
|
||||||
<!-- Models -->
|
<!-- Models -->
|
||||||
<div x-show="$store.global.activeTab === 'models'" x-load-view="'models'"
|
<div x-show="$store.global.activeTab === 'models'" x-load-view="'models'"
|
||||||
x-transition:enter="fade-enter-active" x-transition:enter-start="fade-enter-from"
|
x-transition:enter="fade-enter-active" x-transition:enter-start="fade-enter-from"
|
||||||
class="max-w-7xl mx-auto h-full"></div>
|
class="w-full"></div>
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
<div x-show="$store.global.activeTab === 'logs'" x-load-view="'logs'" x-transition:enter="fade-enter-active"
|
<div x-show="$store.global.activeTab === 'logs'" x-load-view="'logs'" x-transition:enter="fade-enter-active"
|
||||||
x-transition:enter-start="fade-enter-from" class="h-full"></div>
|
x-transition:enter-start="fade-enter-from" class="w-full h-full"></div>
|
||||||
|
|
||||||
<!-- Accounts -->
|
<!-- Accounts -->
|
||||||
<div x-show="$store.global.activeTab === 'accounts'" x-load-view="'accounts'"
|
<div x-show="$store.global.activeTab === 'accounts'" x-load-view="'accounts'"
|
||||||
x-transition:enter="fade-enter-active" x-transition:enter-start="fade-enter-from"
|
x-transition:enter="fade-enter-active" x-transition:enter-start="fade-enter-from"
|
||||||
class="max-w-4xl mx-auto h-full"></div>
|
class="w-full"></div>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<div x-show="$store.global.activeTab === 'settings'" x-load-view="'settings'"
|
<div x-show="$store.global.activeTab === 'settings'" x-load-view="'settings'"
|
||||||
x-transition:enter="fade-enter-active" x-transition:enter-start="fade-enter-from"
|
x-transition:enter="fade-enter-active" x-transition:enter-start="fade-enter-from"
|
||||||
class="max-w-2xl mx-auto h-full"></div>
|
class="w-full"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -309,6 +309,42 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<!-- OAuth Progress Modal -->
|
||||||
|
<dialog id="oauth_progress_modal" class="modal" :class="{ 'modal-open': $store.global.oauthProgress.active }">
|
||||||
|
<div class="modal-box bg-space-900 border border-neon-purple/50">
|
||||||
|
<h3 class="font-bold text-lg text-white flex items-center gap-2">
|
||||||
|
<svg class="w-6 h-6 text-neon-purple animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<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('oauthWaiting')">Waiting for OAuth...</span>
|
||||||
|
</h3>
|
||||||
|
<p class="py-4 text-gray-400 text-sm" x-text="$store.global.t('oauthWaitingDesc')">
|
||||||
|
Please complete authentication in the popup window.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div class="w-full bg-space-800 rounded-full h-2 mb-4 overflow-hidden">
|
||||||
|
<div class="bg-neon-purple h-2 rounded-full transition-all duration-500"
|
||||||
|
:style="`width: ${($store.global.oauthProgress.current / $store.global.oauthProgress.max) * 100}%`">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Text -->
|
||||||
|
<div class="flex justify-between text-xs text-gray-600 mb-4">
|
||||||
|
<span x-text="`${$store.global.oauthProgress.current} / ${$store.global.oauthProgress.max}s`"></span>
|
||||||
|
<span x-text="`${Math.round(($store.global.oauthProgress.current / $store.global.oauthProgress.max) * 100)}%`"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button class="btn btn-sm btn-ghost text-gray-400"
|
||||||
|
@click="$store.global.oauthProgress.cancel && $store.global.oauthProgress.cancel()"
|
||||||
|
x-text="$store.global.t('cancelOAuth')">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!-- Scripts - Loading Order Matters! -->
|
<!-- Scripts - Loading Order Matters! -->
|
||||||
<!-- 1. Utils (global helpers) -->
|
<!-- 1. Utils (global helpers) -->
|
||||||
<script src="js/utils.js"></script>
|
<script src="js/utils.js"></script>
|
||||||
|
|||||||
@@ -5,6 +5,36 @@
|
|||||||
window.Components = window.Components || {};
|
window.Components = window.Components || {};
|
||||||
|
|
||||||
window.Components.accountManager = () => ({
|
window.Components.accountManager = () => ({
|
||||||
|
searchQuery: '',
|
||||||
|
deleteTarget: '',
|
||||||
|
|
||||||
|
get filteredAccounts() {
|
||||||
|
const accounts = Alpine.store('data').accounts || [];
|
||||||
|
if (!this.searchQuery || this.searchQuery.trim() === '') {
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.searchQuery.toLowerCase().trim();
|
||||||
|
return accounts.filter(acc => {
|
||||||
|
return acc.email.toLowerCase().includes(query) ||
|
||||||
|
(acc.projectId && acc.projectId.toLowerCase().includes(query)) ||
|
||||||
|
(acc.source && acc.source.toLowerCase().includes(query));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
formatEmail(email) {
|
||||||
|
if (!email || email.length <= 40) return email;
|
||||||
|
|
||||||
|
const [user, domain] = email.split('@');
|
||||||
|
if (!domain) return email;
|
||||||
|
|
||||||
|
// Preserve domain integrity, truncate username if needed
|
||||||
|
if (user.length > 20) {
|
||||||
|
return `${user.substring(0, 10)}...${user.slice(-5)}@${domain}`;
|
||||||
|
}
|
||||||
|
return email;
|
||||||
|
},
|
||||||
|
|
||||||
async refreshAccount(email) {
|
async refreshAccount(email) {
|
||||||
const store = Alpine.store('global');
|
const store = Alpine.store('global');
|
||||||
store.showToast(store.t('refreshingAccount', { email }), 'info');
|
store.showToast(store.t('refreshingAccount', { email }), 'info');
|
||||||
@@ -88,10 +118,16 @@ window.Components.accountManager = () => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteAccount(email) {
|
confirmDeleteAccount(email) {
|
||||||
|
this.deleteTarget = email;
|
||||||
|
document.getElementById('delete_account_modal').showModal();
|
||||||
|
},
|
||||||
|
|
||||||
|
async executeDelete() {
|
||||||
|
const email = this.deleteTarget;
|
||||||
const store = Alpine.store('global');
|
const store = Alpine.store('global');
|
||||||
if (!confirm(store.t('confirmDelete'))) return;
|
|
||||||
const password = store.webuiPassword;
|
const password = store.webuiPassword;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { response, newPassword } = await window.utils.request(`/api/accounts/${encodeURIComponent(email)}`, { method: 'DELETE' }, password);
|
const { response, newPassword } = await window.utils.request(`/api/accounts/${encodeURIComponent(email)}`, { method: 'DELETE' }, password);
|
||||||
if (newPassword) store.webuiPassword = newPassword;
|
if (newPassword) store.webuiPassword = newPassword;
|
||||||
@@ -100,6 +136,8 @@ window.Components.accountManager = () => ({
|
|||||||
if (data.status === 'ok') {
|
if (data.status === 'ok') {
|
||||||
store.showToast(store.t('deletedAccount', { email }), 'success');
|
store.showToast(store.t('deletedAccount', { email }), 'success');
|
||||||
Alpine.store('data').fetchData();
|
Alpine.store('data').fetchData();
|
||||||
|
document.getElementById('delete_account_modal').close();
|
||||||
|
this.deleteTarget = '';
|
||||||
} else {
|
} else {
|
||||||
store.showToast(data.error || store.t('deleteFailed'), 'error');
|
store.showToast(data.error || store.t('deleteFailed'), 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -464,7 +464,9 @@ window.Components.dashboard = () => ({
|
|||||||
|
|
||||||
createDataset(label, data, color, ctx) {
|
createDataset(label, data, color, ctx) {
|
||||||
const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 200);
|
const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 200);
|
||||||
gradient.addColorStop(0, this.hexToRgba(color, 0.3));
|
// Reduced opacity from 0.3 to 0.12 for less visual noise
|
||||||
|
gradient.addColorStop(0, this.hexToRgba(color, 0.12));
|
||||||
|
gradient.addColorStop(0.6, this.hexToRgba(color, 0.05));
|
||||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -472,12 +474,14 @@ window.Components.dashboard = () => ({
|
|||||||
data,
|
data,
|
||||||
borderColor: color,
|
borderColor: color,
|
||||||
backgroundColor: gradient,
|
backgroundColor: gradient,
|
||||||
borderWidth: 2,
|
borderWidth: 2.5, // Slightly thicker line for better visibility
|
||||||
tension: 0.4,
|
tension: 0.35, // Smoother curves
|
||||||
fill: true,
|
fill: true,
|
||||||
pointRadius: 3,
|
pointRadius: 2.5,
|
||||||
pointHoverRadius: 5,
|
pointHoverRadius: 6,
|
||||||
pointBackgroundColor: color
|
pointBackgroundColor: color,
|
||||||
|
pointBorderColor: 'rgba(9, 9, 11, 0.8)',
|
||||||
|
pointBorderWidth: 1.5
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,28 @@ window.Components.logsViewer = () => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
get filteredLogs() {
|
get filteredLogs() {
|
||||||
const query = this.searchQuery.toLowerCase();
|
const query = this.searchQuery.trim();
|
||||||
|
if (!query) {
|
||||||
|
return this.logs.filter(log => this.filters[log.level]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try regex first, fallback to plain text search
|
||||||
|
let matcher;
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(query, 'i');
|
||||||
|
matcher = (msg) => regex.test(msg);
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid regex, fallback to case-insensitive string search
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
matcher = (msg) => msg.toLowerCase().includes(lowerQuery);
|
||||||
|
}
|
||||||
|
|
||||||
return this.logs.filter(log => {
|
return this.logs.filter(log => {
|
||||||
// Level Filter
|
// Level Filter
|
||||||
if (!this.filters[log.level]) return false;
|
if (!this.filters[log.level]) return false;
|
||||||
|
|
||||||
// Search Filter
|
// Search Filter
|
||||||
if (query && !log.message.toLowerCase().includes(query)) return false;
|
return matcher(log.message);
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -76,17 +76,17 @@ document.addEventListener('alpine:init', () => {
|
|||||||
const config = this.modelConfig[modelId] || {};
|
const config = this.modelConfig[modelId] || {};
|
||||||
const family = this.getModelFamily(modelId);
|
const family = this.getModelFamily(modelId);
|
||||||
|
|
||||||
// Visibility Logic for Models Tab (quotaRows):
|
// Visibility Logic for Models Page (quotaRows):
|
||||||
// 1. If explicitly hidden via config, always hide
|
// 1. If explicitly hidden via config, ALWAYS hide (clean interface)
|
||||||
// 2. If no config, default 'unknown' families to HIDDEN
|
// 2. If no config, default 'unknown' families to HIDDEN
|
||||||
// 3. Known families (Claude/Gemini) default to VISIBLE
|
// 3. Known families (Claude/Gemini) default to VISIBLE
|
||||||
// Note: showHiddenModels toggle is for Settings page only, NOT here
|
// Note: To manage hidden models, use Settings → Models tab
|
||||||
let isHidden = config.hidden;
|
let isHidden = config.hidden;
|
||||||
if (isHidden === undefined) {
|
if (isHidden === undefined) {
|
||||||
isHidden = (family === 'other' || family === 'unknown');
|
isHidden = (family === 'other' || family === 'unknown');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Models Tab: ALWAYS hide hidden models (no toggle check)
|
// Models Page: ALWAYS hide hidden models (use Settings to restore)
|
||||||
if (isHidden) return;
|
if (isHidden) return;
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
|
|||||||
@@ -219,6 +219,30 @@ document.addEventListener('alpine:init', () => {
|
|||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
passwordsNotMatch: "Passwords do not match",
|
passwordsNotMatch: "Passwords do not match",
|
||||||
passwordTooShort: "Password must be at least 6 characters",
|
passwordTooShort: "Password must be at least 6 characters",
|
||||||
|
// Dashboard drill-down
|
||||||
|
clickToViewAllAccounts: "Click to view all accounts",
|
||||||
|
clickToViewModels: "Click to view Models page",
|
||||||
|
clickToViewLimitedAccounts: "Click to view rate-limited accounts",
|
||||||
|
clickToFilterClaude: "Click to filter Claude models",
|
||||||
|
clickToFilterGemini: "Click to filter Gemini models",
|
||||||
|
// Accounts page
|
||||||
|
searchAccounts: "Search accounts...",
|
||||||
|
noAccountsYet: "No Accounts Yet",
|
||||||
|
noAccountsDesc: "Get started by adding a Google account via OAuth, or use the CLI command to import credentials.",
|
||||||
|
addFirstAccount: "Add Your First Account",
|
||||||
|
noSearchResults: "No accounts match your search",
|
||||||
|
clearSearch: "Clear Search",
|
||||||
|
disabledAccountsNote: "<strong>Disabled accounts</strong> will not be used for request routing but remain in the configuration. Dashboard statistics only include enabled accounts.",
|
||||||
|
dangerousOperation: "⚠️ Dangerous Operation",
|
||||||
|
confirmDeletePrompt: "Are you sure you want to delete account",
|
||||||
|
deleteWarning: "⚠️ This action cannot be undone. All configuration and historical records will be permanently deleted.",
|
||||||
|
// OAuth progress
|
||||||
|
oauthWaiting: "Waiting for OAuth authorization...",
|
||||||
|
oauthWaitingDesc: "Please complete the authentication in the popup window. This may take up to 2 minutes.",
|
||||||
|
oauthCancelled: "OAuth authorization cancelled",
|
||||||
|
oauthTimeout: "⏱️ OAuth authorization timed out. Please try again.",
|
||||||
|
oauthWindowClosed: "OAuth window was closed. Authorization may be incomplete.",
|
||||||
|
cancelOAuth: "Cancel",
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
dashboard: "仪表盘",
|
dashboard: "仪表盘",
|
||||||
@@ -427,12 +451,44 @@ document.addEventListener('alpine:init', () => {
|
|||||||
cancel: "取消",
|
cancel: "取消",
|
||||||
passwordsNotMatch: "密码不匹配",
|
passwordsNotMatch: "密码不匹配",
|
||||||
passwordTooShort: "密码至少需要 6 个字符",
|
passwordTooShort: "密码至少需要 6 个字符",
|
||||||
|
// Dashboard drill-down
|
||||||
|
clickToViewAllAccounts: "点击查看所有账号",
|
||||||
|
clickToViewModels: "点击查看模型页面",
|
||||||
|
clickToViewLimitedAccounts: "点击查看受限账号",
|
||||||
|
clickToFilterClaude: "点击筛选 Claude 模型",
|
||||||
|
clickToFilterGemini: "点击筛选 Gemini 模型",
|
||||||
|
// 账号页面
|
||||||
|
searchAccounts: "搜索账号...",
|
||||||
|
noAccountsYet: "还没有添加任何账号",
|
||||||
|
noAccountsDesc: "点击上方的 \"添加节点\" 按钮通过 OAuth 添加 Google 账号,或者使用 CLI 命令导入凭证。",
|
||||||
|
addFirstAccount: "添加第一个账号",
|
||||||
|
noSearchResults: "没有找到匹配的账号",
|
||||||
|
clearSearch: "清除搜索",
|
||||||
|
disabledAccountsNote: "<strong>已禁用的账号</strong>不会用于请求路由,但仍保留在配置中。仪表盘统计数据仅包含已启用的账号。",
|
||||||
|
dangerousOperation: "⚠️ 危险操作",
|
||||||
|
confirmDeletePrompt: "确定要删除账号",
|
||||||
|
deleteWarning: "⚠️ 此操作不可撤销,账号的所有配置和历史记录将永久删除。",
|
||||||
|
// OAuth 进度
|
||||||
|
oauthWaiting: "等待 OAuth 授权中...",
|
||||||
|
oauthWaitingDesc: "请在弹出窗口中完成认证。此过程最长可能需要 2 分钟。",
|
||||||
|
oauthCancelled: "已取消 OAuth 授权",
|
||||||
|
oauthTimeout: "⏱️ OAuth 授权超时,请重试。",
|
||||||
|
oauthWindowClosed: "OAuth 窗口已关闭,授权可能未完成。",
|
||||||
|
cancelOAuth: "取消",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toast Messages
|
// Toast Messages
|
||||||
toast: null,
|
toast: null,
|
||||||
|
|
||||||
|
// OAuth Progress
|
||||||
|
oauthProgress: {
|
||||||
|
active: false,
|
||||||
|
current: 0,
|
||||||
|
max: 60,
|
||||||
|
cancel: null
|
||||||
|
},
|
||||||
|
|
||||||
t(key, params = {}) {
|
t(key, params = {}) {
|
||||||
let str = this.translations[this.lang][key] || key;
|
let str = this.translations[this.lang][key] || key;
|
||||||
if (typeof str === 'string') {
|
if (typeof str === 'string') {
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
<div x-data="accountManager" class="view-container">
|
<div x-data="accountManager" class="view-container">
|
||||||
<!-- Header -->
|
<!-- Compact Header -->
|
||||||
<div class="flex items-start justify-between mb-6">
|
<div class="flex items-center justify-between gap-4 mb-6">
|
||||||
<div>
|
<!-- Title with inline subtitle -->
|
||||||
<h2 class="text-2xl font-bold text-white tracking-tight mb-1" x-text="$store.global.t('accessCredentials')">
|
<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
|
Access Credentials
|
||||||
</h2>
|
</h1>
|
||||||
<p class="text-sm text-gray-500" x-text="$store.global.t('manageTokens')">
|
<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
|
Manage OAuth tokens and session states
|
||||||
</p>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
<div class="flex items-center gap-2">
|
<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()">
|
@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" />
|
<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>
|
</svg>
|
||||||
<span x-text="$store.global.t('reload')">Reload</span>
|
<span x-text="$store.global.t('reload')">Reload</span>
|
||||||
</button>
|
</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()">
|
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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
<span x-text="$store.global.t('addNode')">Add Node</span>
|
<span x-text="$store.global.t('addNode')">Add Node</span>
|
||||||
@@ -27,21 +42,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table Container -->
|
<!-- Table Card -->
|
||||||
<div class="bg-space-900/40 border border-space-border/30 rounded-xl overflow-hidden backdrop-blur-sm">
|
<div class="view-card !p-0">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead x-show="$store.data.accounts.length > 0">
|
||||||
<tr class="border-b border-space-border/50">
|
<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-20" x-text="$store.global.t('enabled')">Enabled</th>
|
<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" 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 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-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-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-32" x-text="$store.global.t('projectId')">Project ID</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 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>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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">
|
<tr class="border-b border-space-border/30 last:border-0 hover:bg-white/5 transition-colors group">
|
||||||
<td class="pl-6 py-4">
|
<td class="pl-6 py-4">
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
@@ -54,9 +102,7 @@
|
|||||||
<td class="py-4">
|
<td class="py-4">
|
||||||
<div class="tooltip tooltip-right" :data-tip="acc.email">
|
<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"
|
<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 ?
|
x-text="formatEmail(acc.email)">
|
||||||
acc.email.substring(0, 15) + '...' + acc.email.slice(-18) :
|
|
||||||
acc.email">
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -66,7 +112,6 @@
|
|||||||
x-text="acc.source || 'oauth'">
|
x-text="acc.source || 'oauth'">
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 font-mono text-xs text-gray-500" x-text="acc.projectId || '-'"></td>
|
|
||||||
<td class="py-4">
|
<td class="py-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-2 h-2 rounded-full flex-shrink-0"
|
<div class="w-2 h-2 rounded-full flex-shrink-0"
|
||||||
@@ -81,7 +126,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 pr-6">
|
<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 -->
|
<!-- Fix Button -->
|
||||||
<button x-show="acc.status === 'invalid'"
|
<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"
|
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="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'"
|
: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'"
|
: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')">
|
: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">
|
<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"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
@@ -111,7 +156,71 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
@@ -1,10 +1,39 @@
|
|||||||
<div x-data="dashboard" class="view-container">
|
<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 -->
|
<!-- 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
|
<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 移到右上角,缩小并变灰 -->
|
<!-- 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">
|
<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"
|
<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">
|
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-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"
|
<div class="stat-title text-gray-500 font-mono text-xs uppercase tracking-wider truncate"
|
||||||
x-text="$store.global.t('totalAccounts')"></div>
|
x-text="$store.global.t('totalAccounts')"></div>
|
||||||
<div class="stat-desc text-gray-600 text-[10px] truncate" x-text="$store.global.t('registeredNodes')">
|
<div class="stat-desc text-cyan-400/60 text-[10px] truncate flex items-center gap-1">
|
||||||
Registered
|
<span x-text="$store.global.t('registeredNodes')"></span>
|
||||||
Nodes</div>
|
<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>
|
||||||
|
|
||||||
<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">
|
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"
|
||||||
<div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-neon-green/50 transition-colors">
|
@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">
|
<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"
|
<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>
|
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-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"
|
<div class="stat-title text-gray-500 font-mono text-xs uppercase tracking-wider truncate"
|
||||||
x-text="$store.global.t('active')"></div>
|
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>
|
</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">
|
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"
|
||||||
<div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-red-500/50 transition-colors">
|
@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">
|
<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"
|
<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>
|
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-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"
|
<div class="stat-title text-gray-500 font-mono text-xs uppercase tracking-wider truncate"
|
||||||
x-text="$store.global.t('rateLimited')"></div>
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Global Quota Chart -->
|
<!-- Global Quota Chart -->
|
||||||
<div
|
<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 -->
|
<!-- 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>
|
<canvas id="quotaChart"></canvas>
|
||||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
<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 class="text-[10px] font-bold text-white font-mono" x-text="stats.overallHealth + '%'">%</div>
|
||||||
@@ -61,7 +106,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Legend / Info -->
|
<!-- 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">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-[10px] text-gray-500 uppercase tracking-wider font-mono truncate"
|
<span class="text-[10px] text-gray-500 uppercase tracking-wider font-mono truncate"
|
||||||
x-text="$store.global.t('globalQuota')">Global Quota</span>
|
x-text="$store.global.t('globalQuota')">Global Quota</span>
|
||||||
@@ -69,35 +114,37 @@
|
|||||||
|
|
||||||
<!-- Custom Legend -->
|
<!-- Custom Legend -->
|
||||||
<div class="space-y-1">
|
<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="flex items-center gap-1.5 truncate">
|
||||||
<div
|
<div class="w-1.5 h-1.5 rounded-full bg-neon-purple flex-shrink-0"></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>
|
|
||||||
<span class="truncate" x-text="$store.global.t('familyClaude')">Claude</span>
|
<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>
|
</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="flex items-center gap-1.5 truncate">
|
||||||
<div
|
<div class="w-1.5 h-1.5 rounded-full bg-neon-green flex-shrink-0"></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>
|
|
||||||
<span class="truncate" x-text="$store.global.t('familyGemini')">Gemini</span>
|
<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>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Usage Trend Chart -->
|
<!-- Usage Trend Chart -->
|
||||||
<div class="glass-panel p-4 rounded-lg">
|
<div class="view-card">
|
||||||
<!-- Header with Stats and Filter -->
|
<!-- 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 flex-wrap items-center gap-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
@@ -111,17 +158,17 @@
|
|||||||
|
|
||||||
<!-- Usage Stats Pills -->
|
<!-- Usage Stats Pills -->
|
||||||
<div class="flex flex-wrap gap-2 text-[10px] font-mono">
|
<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-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>
|
||||||
<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-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>
|
||||||
<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-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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,24 +62,33 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Log Content -->
|
<!-- 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">
|
<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">
|
<div class="flex gap-4 px-2 py-0.5 -mx-2 hover:bg-white/[0.03] transition-colors group">
|
||||||
<span class="text-gray-600 w-16 shrink-0 select-none group-hover:text-gray-500"
|
<!-- 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>
|
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',
|
<!-- Level: Tag Style -->
|
||||||
'text-yellow-400': log.level === 'WARN',
|
<div class="w-14 shrink-0 flex items-center">
|
||||||
'text-red-500': log.level === 'ERROR',
|
<span class="px-1.5 py-0.5 rounded-[2px] text-[10px] font-bold uppercase tracking-wider leading-none border"
|
||||||
'text-neon-green': log.level === 'SUCCESS',
|
:class="{
|
||||||
'text-purple-400': log.level === 'DEBUG'
|
'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>
|
}" x-text="log.level"></span>
|
||||||
<span class="text-gray-300 break-all" x-html="log.message.replace(/\n/g, '<br>')"></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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- Blinking Cursor -->
|
<!-- 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 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-gray-600 italic mt-4 text-center"
|
<div x-show="filteredLogs.length === 0 && logs.length > 0" class="text-zinc-700 italic mt-8 text-center"
|
||||||
x-text="$store.global.t('noLogsMatch')">
|
x-text="$store.global.t('noLogsMatch')">
|
||||||
No logs match filter
|
No logs match filter
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,48 @@
|
|||||||
<div x-data="models" class="view-container">
|
<div x-data="models" class="view-container">
|
||||||
<!-- Header -->
|
<!-- Compact Header -->
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between gap-4 mb-6">
|
||||||
<div>
|
<!-- Title with inline subtitle -->
|
||||||
<h2 class="text-xl font-bold text-white" x-text="$store.global.t('models')">Models</h2>
|
<div class="flex items-baseline gap-3">
|
||||||
<p class="text-sm text-gray-500" x-text="$store.global.t('modelsPageDesc')">Real-time quota and status for
|
<h1 class="text-2xl font-bold text-white tracking-tight" x-text="$store.global.t('models')">
|
||||||
all available models.</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controls -->
|
<!-- 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="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">
|
<div class="flex flex-col md:flex-row items-center gap-4 w-full lg:w-auto flex-wrap">
|
||||||
<!-- Custom Select -->
|
<!-- Custom Select -->
|
||||||
<div class="relative w-full md:w-64 h-10">
|
<div class="relative w-full md:w-64 h-9">
|
||||||
<select
|
<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()">
|
x-model="$store.data.filters.account" @change="$store.data.computeQuotaRows()">
|
||||||
<option value="all" x-text="$store.global.t('allAccounts')">All Accounts</option>
|
<option value="all" x-text="$store.global.t('allAccounts')">All Accounts</option>
|
||||||
<template x-for="acc in $store.data.accounts" :key="acc.email">
|
<template x-for="acc in $store.data.accounts" :key="acc.email">
|
||||||
@@ -29,41 +57,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Buttons -->
|
<!-- 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
|
<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="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="{'bg-neon-purple text-white border-neon-purple hover:bg-purple-600 hover:border-purple-500': $store.data.filters.family === 'all'}"
|
: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()"
|
@click="$store.data.filters.family = 'all'; $store.data.computeQuotaRows()"
|
||||||
x-text="$store.global.t('allCaps')">ALL</button>
|
x-text="$store.global.t('allCaps')">ALL</button>
|
||||||
<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="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="{'bg-neon-purple text-white border-neon-purple hover:bg-purple-600 hover:border-purple-500': $store.data.filters.family === 'claude'}"
|
: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()"
|
@click="$store.data.filters.family = 'claude'; $store.data.computeQuotaRows()"
|
||||||
x-text="$store.global.t('claudeCaps')">CLAUDE</button>
|
x-text="$store.global.t('claudeCaps')">CLAUDE</button>
|
||||||
<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="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="{'bg-neon-purple text-white border-neon-purple hover:bg-purple-600 hover:border-purple-500': $store.data.filters.family === 'gemini'}"
|
: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()"
|
@click="$store.data.filters.family = 'gemini'; $store.data.computeQuotaRows()"
|
||||||
x-text="$store.global.t('geminiCaps')">GEMINI</button>
|
x-text="$store.global.t('geminiCaps')">GEMINI</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Main Table -->
|
<!-- Main Table Card -->
|
||||||
<div class="glass-panel rounded-xl overflow-x-auto min-h-[400px]">
|
<div class="view-card !p-0">
|
||||||
|
<div class="overflow-x-auto min-h-[400px]">
|
||||||
<table class="standard-table"
|
<table class="standard-table"
|
||||||
:class="{'table-xs': $store.settings.compact, 'table-sm': !$store.settings.compact}">
|
:class="{'table-xs': $store.settings.compact, 'table-sm': !$store.settings.compact}">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -100,6 +122,9 @@
|
|||||||
<div class="flex justify-between text-xs font-mono">
|
<div class="flex justify-between text-xs font-mono">
|
||||||
<span x-text="row.avgQuota + '%'"
|
<span x-text="row.avgQuota + '%'"
|
||||||
:class="row.avgQuota > 0 ? 'text-white' : 'text-red-500'"></span>
|
: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>
|
</div>
|
||||||
<progress class="progress w-full h-1 bg-space-800"
|
<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')"
|
:class="row.avgQuota > 50 ? 'progress-gradient-success' : (row.avgQuota > 0 ? 'progress-gradient-warning' : 'progress-gradient-error')"
|
||||||
@@ -107,8 +132,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-xs">
|
<td class="font-mono text-xs">
|
||||||
|
<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(' ');
|
||||||
|
return resets || 'No reset scheduled';
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
:data-tip="tooltipText">
|
||||||
<span x-text="row.resetIn"
|
<span x-text="row.resetIn"
|
||||||
:class="(row.resetIn && row.resetIn.indexOf('h') === -1 && row.resetIn !== '-') ? 'text-neon-purple font-bold' : 'text-gray-400'"></span>
|
:class="(row.resetIn && row.resetIn.indexOf('h') === -1 && row.resetIn !== '-') ? 'text-neon-purple font-bold' : 'text-gray-400'"></span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex items-center justify-end gap-3">
|
<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]">
|
<div class="flex flex-wrap gap-1 justify-end max-w-[200px]">
|
||||||
<template x-for="q in row.quotaInfo" :key="q.fullEmail">
|
<template x-for="q in row.quotaInfo" :key="q.fullEmail">
|
||||||
<div class="tooltip tooltip-left" :data-tip="q.fullEmail + ' (' + q.pct + '%)">
|
<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')">
|
:class="q.pct > 50 ? 'bg-neon-green opacity-80' : (q.pct > 0 ? 'bg-yellow-500 opacity-80' : 'bg-red-900 opacity-50')">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,13 +174,18 @@
|
|||||||
<button class="btn btn-xs btn-circle transition-colors"
|
<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'"
|
: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 })"
|
@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"
|
<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" />
|
<path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg x-show="!row.pinned" xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5"
|
<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"
|
<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" />
|
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -151,16 +194,21 @@
|
|||||||
<button class="btn btn-xs btn-circle transition-colors"
|
<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'"
|
: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 })"
|
@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"
|
<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"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<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" />
|
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>
|
||||||
<svg x-show="row.hidden" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4"
|
<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"
|
<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" />
|
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>
|
</svg>
|
||||||
@@ -191,3 +239,4 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
@@ -73,12 +73,16 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="join w-full grid grid-cols-2">
|
<div class="join w-full grid grid-cols-2">
|
||||||
<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="join-item btn btn-sm border-space-border/50 bg-space-800 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="$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>
|
@click="$store.global.setLang('en')">English</button>
|
||||||
<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="join-item btn btn-sm border-space-border/50 bg-space-800 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="$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>
|
@click="$store.global.setLang('zh')">中文</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,10 +95,16 @@
|
|||||||
<span class="label-text-alt font-mono text-neon-purple"
|
<span class="label-text-alt font-mono text-neon-purple"
|
||||||
x-text="$store.settings.refreshInterval + 's'"></span>
|
x-text="$store.settings.refreshInterval + 's'"></span>
|
||||||
</label>
|
</label>
|
||||||
<input type="range" min="10" max="300" class="custom-range custom-range-purple"
|
<div class="flex gap-3 items-center">
|
||||||
x-model="$store.settings.refreshInterval"
|
<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%`"
|
:style="`background-size: ${($store.settings.refreshInterval - 10) / 2.9}% 100%`"
|
||||||
@change="$store.settings.saveSettings(true)">
|
@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">
|
<div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono">
|
||||||
<span>10s</span>
|
<span>10s</span>
|
||||||
<span>300s</span>
|
<span>300s</span>
|
||||||
@@ -109,10 +119,16 @@
|
|||||||
<span class="label-text-alt font-mono text-neon-purple"
|
<span class="label-text-alt font-mono text-neon-purple"
|
||||||
x-text="$store.settings.logLimit + ' ' + $store.global.t('lines')"></span>
|
x-text="$store.settings.logLimit + ' ' + $store.global.t('lines')"></span>
|
||||||
</label>
|
</label>
|
||||||
<input type="range" min="500" max="5000" step="500" class="custom-range custom-range-purple"
|
<div class="flex gap-3 items-center">
|
||||||
x-model="$store.settings.logLimit"
|
<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%`"
|
:style="`background-size: ${($store.settings.logLimit - 500) / 45}% 100%`"
|
||||||
@change="$store.settings.saveSettings(true)">
|
@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">
|
<div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono">
|
||||||
<span>500</span>
|
<span>500</span>
|
||||||
<span>5000</span>
|
<span>5000</span>
|
||||||
@@ -406,15 +422,14 @@
|
|||||||
<!-- Tab 3: Models Configuration -->
|
<!-- Tab 3: Models Configuration -->
|
||||||
<div x-show="activeTab === 'models'" x-data="window.Components.modelManager()"
|
<div x-show="activeTab === 'models'" x-data="window.Components.modelManager()"
|
||||||
class="space-y-6 max-w-3xl animate-fade-in">
|
class="space-y-6 max-w-3xl animate-fade-in">
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm text-gray-400" x-text="$store.global.t('modelsDesc')">Manage visibility and
|
<div class="text-sm text-gray-400" x-text="$store.global.t('modelsDesc')">Configure model visibility, pinning, and request mapping.</div>
|
||||||
ordering of models in the dashboard.</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 class="text-xs text-gray-600 mt-1" x-text="$store.global.t('modelMappingHint')"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs text-gray-500" x-text="$store.global.t('showHidden')">Show Hidden
|
<span class="text-xs text-gray-500" x-text="$store.global.t('showHidden')">Show Hidden Models</span>
|
||||||
Models</span>
|
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<input type="checkbox" class="sr-only peer"
|
<input type="checkbox" class="sr-only peer"
|
||||||
:checked="$store.settings.showHiddenModels === true"
|
:checked="$store.settings.showHiddenModels === true"
|
||||||
@@ -431,17 +446,17 @@
|
|||||||
<table class="standard-table">
|
<table class="standard-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="pl-4 w-1/2" x-text="$store.global.t('modelId')">Model ID</th>
|
<th class="pl-4 w-5/12" 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 class="w-5/12" x-text="$store.global.t('modelMapping')">Mapping (Target Model ID)</th>
|
||||||
</th>
|
<th class="w-2/12 text-right pr-4" x-text="$store.global.t('actions')">Actions</th>
|
||||||
<th class="text-right pr-4 w-24" x-text="$store.global.t('actions')">Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-for="modelId in $store.data.models" :key="modelId">
|
<template x-for="modelId in $store.data.models" :key="modelId">
|
||||||
<tr class="hover:bg-white/5 transition-colors group"
|
<tr class="hover:bg-white/5 transition-colors group"
|
||||||
:class="isHidden ? 'opacity-50' : ''"
|
:class="isHidden ? 'opacity-50' : ''"
|
||||||
x-show="!isHidden || $store.settings.showHiddenModels" x-data="{
|
x-show="!isHidden || $store.settings.showHiddenModels"
|
||||||
|
x-data="{
|
||||||
newMapping: '',
|
newMapping: '',
|
||||||
get config() { return $store.data.modelConfig[modelId] || {} },
|
get config() { return $store.data.modelConfig[modelId] || {} },
|
||||||
get isPinned() { return !!this.config.pinned },
|
get isPinned() { return !!this.config.pinned },
|
||||||
@@ -451,13 +466,20 @@
|
|||||||
return (family === 'other' || family === 'unknown');
|
return (family === 'other' || family === 'unknown');
|
||||||
}
|
}
|
||||||
}" x-init="newMapping = config.mapping || ''">
|
}" 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>
|
<td>
|
||||||
<div x-show="!isEditing(modelId)"
|
<div x-show="!isEditing(modelId)"
|
||||||
class="flex items-center gap-2 group-hover:text-white transition-colors cursor-pointer"
|
class="flex items-center gap-2 group-hover:text-white transition-colors cursor-pointer py-2"
|
||||||
@click="startEditing(modelId); newMapping = config.mapping || ''">
|
@click="startEditing(modelId); newMapping = config.mapping || ''; $nextTick(() => $refs['input-' + modelId]?.focus())">
|
||||||
<span x-text="config.mapping || '-'"
|
<span x-text="config.mapping || 'Click to set...'"
|
||||||
:class="{'text-gray-600 italic': !config.mapping}"></span>
|
: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"
|
<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">
|
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
@@ -466,24 +488,39 @@
|
|||||||
</div>
|
</div>
|
||||||
<div x-show="isEditing(modelId)" class="flex items-center gap-1">
|
<div x-show="isEditing(modelId)" class="flex items-center gap-1">
|
||||||
<input type="text" x-model="newMapping"
|
<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"
|
:x-ref="'input-' + modelId"
|
||||||
placeholder="e.g. claude-sonnet-4-5"
|
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.enter="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()"
|
||||||
@keydown.escape="newMapping = config.mapping || ''; stopEditing()">
|
@keydown.escape="newMapping = config.mapping || ''; stopEditing()">
|
||||||
<button class="btn btn-xs btn-ghost btn-square text-green-500"
|
<button class="btn btn-xs btn-ghost btn-square text-green-500 hover:bg-green-500/20"
|
||||||
@click="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()"><svg
|
@click="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()"
|
||||||
xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
|
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">
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
stroke-width="2" d="M5 13l4 4L19 7" />
|
stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
</svg></button>
|
</svg>
|
||||||
<button class="btn btn-xs btn-ghost btn-square text-gray-500"
|
</button>
|
||||||
@click="newMapping = config.mapping || ''; stopEditing()"><svg
|
<button class="btn btn-xs btn-ghost btn-square text-gray-500 hover:bg-gray-500/20"
|
||||||
xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
|
@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">
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right pr-4">
|
<td class="text-right pr-4">
|
||||||
@@ -533,7 +570,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<tr x-show="!$store.data.models.length">
|
<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')">
|
x-text="$store.global.t('noModels')">
|
||||||
NO MODELS DETECTED
|
NO MODELS DETECTED
|
||||||
</td>
|
</td>
|
||||||
@@ -675,10 +712,16 @@
|
|||||||
<span class="label-text-alt font-mono text-neon-purple text-xs font-semibold"
|
<span class="label-text-alt font-mono text-neon-purple text-xs font-semibold"
|
||||||
x-text="serverConfig.maxRetries || 5"></span>
|
x-text="serverConfig.maxRetries || 5"></span>
|
||||||
</label>
|
</label>
|
||||||
<input type="range" min="1" max="20" class="custom-range custom-range-purple"
|
<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"
|
:value="serverConfig.maxRetries || 5"
|
||||||
:style="`background-size: ${((serverConfig.maxRetries || 5) - 1) / 19 * 100}% 100%`"
|
:style="`background-size: ${((serverConfig.maxRetries || 5) - 1) / 19 * 100}% 100%`"
|
||||||
@input="toggleMaxRetries($event.target.value)">
|
@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>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<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"
|
<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>
|
x-text="((serverConfig.retryBaseMs || 1000) < 10000 ? (serverConfig.retryBaseMs || 1000) + 'ms' : Math.round((serverConfig.retryBaseMs || 1000) / 1000) + 's')"></span>
|
||||||
</label>
|
</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
<input type="range" min="100" max="10000" step="100"
|
<input type="range" min="100" max="10000" step="100"
|
||||||
class="custom-range custom-range-green"
|
class="custom-range custom-range-green flex-1"
|
||||||
:value="serverConfig.retryBaseMs || 1000"
|
:value="serverConfig.retryBaseMs || 1000"
|
||||||
:style="`background-size: ${((serverConfig.retryBaseMs || 1000) - 100) / 99}% 100%`"
|
:style="`background-size: ${((serverConfig.retryBaseMs || 1000) - 100) / 99}% 100%`"
|
||||||
@input="toggleRetryBaseMs($event.target.value)">
|
@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>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label pt-0">
|
<label class="label pt-0">
|
||||||
@@ -702,11 +751,17 @@
|
|||||||
<span class="label-text-alt font-mono text-neon-green text-xs font-semibold"
|
<span class="label-text-alt font-mono text-neon-green text-xs font-semibold"
|
||||||
x-text="Math.round((serverConfig.retryMaxMs || 30000) / 1000) + 's'"></span>
|
x-text="Math.round((serverConfig.retryMaxMs || 30000) / 1000) + 's'"></span>
|
||||||
</label>
|
</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
<input type="range" min="1000" max="120000" step="1000"
|
<input type="range" min="1000" max="120000" step="1000"
|
||||||
class="custom-range custom-range-green"
|
class="custom-range custom-range-green flex-1"
|
||||||
:value="serverConfig.retryMaxMs || 30000"
|
:value="serverConfig.retryMaxMs || 30000"
|
||||||
:style="`background-size: ${((serverConfig.retryMaxMs || 30000) - 1000) / 1190}% 100%`"
|
:style="`background-size: ${((serverConfig.retryMaxMs || 30000) - 1000) / 1190}% 100%`"
|
||||||
@input="toggleRetryMaxMs($event.target.value)">
|
@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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -725,11 +780,17 @@
|
|||||||
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
|
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
|
||||||
x-text="Math.round((serverConfig.defaultCooldownMs || 60000) / 1000) + 's'"></span>
|
x-text="Math.round((serverConfig.defaultCooldownMs || 60000) / 1000) + 's'"></span>
|
||||||
</label>
|
</label>
|
||||||
|
<div class="flex gap-3 items-center">
|
||||||
<input type="range" min="1000" max="300000" step="1000"
|
<input type="range" min="1000" max="300000" step="1000"
|
||||||
class="custom-range custom-range-cyan"
|
class="custom-range custom-range-cyan flex-1"
|
||||||
:value="serverConfig.defaultCooldownMs || 60000"
|
:value="serverConfig.defaultCooldownMs || 60000"
|
||||||
:style="`background-size: ${((serverConfig.defaultCooldownMs || 60000) - 1000) / 2990}% 100%`"
|
:style="`background-size: ${((serverConfig.defaultCooldownMs || 60000) - 1000) / 2990}% 100%`"
|
||||||
@input="toggleDefaultCooldownMs($event.target.value)">
|
@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>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
@@ -739,11 +800,17 @@
|
|||||||
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
|
<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>
|
x-text="((serverConfig.maxWaitBeforeErrorMs || 120000) >= 60000 ? Math.round((serverConfig.maxWaitBeforeErrorMs || 120000) / 60000) + 'm' : Math.round((serverConfig.maxWaitBeforeErrorMs || 120000) / 1000) + 's')"></span>
|
||||||
</label>
|
</label>
|
||||||
|
<div class="flex gap-3 items-center">
|
||||||
<input type="range" min="0" max="600000" step="10000"
|
<input type="range" min="0" max="600000" step="10000"
|
||||||
class="custom-range custom-range-cyan"
|
class="custom-range custom-range-cyan flex-1"
|
||||||
:value="serverConfig.maxWaitBeforeErrorMs || 120000"
|
:value="serverConfig.maxWaitBeforeErrorMs || 120000"
|
||||||
:style="`background-size: ${(serverConfig.maxWaitBeforeErrorMs || 120000) / 6000}% 100%`"
|
:style="`background-size: ${(serverConfig.maxWaitBeforeErrorMs || 120000) / 6000}% 100%`"
|
||||||
@input="toggleMaxWaitBeforeErrorMs($event.target.value)">
|
@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"
|
<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
|
x-text="$store.global.t('maxWaitDesc')">Maximum time to wait for a sticky account to
|
||||||
reset before switching.</p>
|
reset before switching.</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user