Files
antigravity-claude-proxy/public/js/components/account-manager.js
jgor20 52c3fa5669 refactor(ui): improve model quota selection logic
Introduce a priority-based system for selecting the main model quota, considering model tiers (e.g., opus, sonnet, pro) and availability status. This enhances accuracy by treating models with less than 1% remaining quota as "dead" for prioritization, ensuring users see the most relevant and active model in the dashboard.
2026-01-11 13:12:14 +00:00

234 lines
8.7 KiB
JavaScript

/**
* Account Manager Component
* Registers itself to window.Components for Alpine.js to consume
*/
window.Components = window.Components || {};
window.Components.accountManager = () => ({
searchQuery: '',
deleteTarget: '',
refreshing: false,
toggling: false,
deleting: false,
reloading: false,
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) {
return await window.ErrorHandler.withLoading(async () => {
const store = Alpine.store('global');
store.showToast(store.t('refreshingAccount', { email }), 'info');
const { response, newPassword } = await window.utils.request(
`/api/accounts/${encodeURIComponent(email)}/refresh`,
{ method: 'POST' },
store.webuiPassword
);
if (newPassword) store.webuiPassword = newPassword;
const data = await response.json();
if (data.status === 'ok') {
store.showToast(store.t('refreshedAccount', { email }), 'success');
Alpine.store('data').fetchData();
} else {
throw new Error(data.error || store.t('refreshFailed'));
}
}, this, 'refreshing', { errorMessage: 'Failed to refresh account' });
},
async toggleAccount(email, enabled) {
const store = Alpine.store('global');
const password = store.webuiPassword;
// Optimistic update: immediately update UI
const dataStore = Alpine.store('data');
const account = dataStore.accounts.find(a => a.email === email);
if (account) {
account.enabled = enabled;
}
try {
const { response, newPassword } = await window.utils.request(`/api/accounts/${encodeURIComponent(email)}/toggle`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
}, password);
if (newPassword) store.webuiPassword = newPassword;
const data = await response.json();
if (data.status === 'ok') {
const status = enabled ? store.t('enabledStatus') : store.t('disabledStatus');
store.showToast(store.t('accountToggled', { email, status }), 'success');
// Refresh to confirm server state
await dataStore.fetchData();
} else {
store.showToast(data.error || store.t('toggleFailed'), 'error');
// Rollback optimistic update on error
if (account) {
account.enabled = !enabled;
}
await dataStore.fetchData();
}
} catch (e) {
store.showToast(store.t('toggleFailed') + ': ' + e.message, 'error');
// Rollback optimistic update on error
if (account) {
account.enabled = !enabled;
}
await dataStore.fetchData();
}
},
async fixAccount(email) {
const store = Alpine.store('global');
store.showToast(store.t('reauthenticating', { email }), 'info');
const password = store.webuiPassword;
try {
const urlPath = `/api/auth/url?email=${encodeURIComponent(email)}`;
const { response, newPassword } = await window.utils.request(urlPath, {}, password);
if (newPassword) store.webuiPassword = newPassword;
const data = await response.json();
if (data.status === 'ok') {
window.open(data.url, 'google_oauth', 'width=600,height=700,scrollbars=yes');
} else {
store.showToast(data.error || store.t('authUrlFailed'), 'error');
}
} catch (e) {
store.showToast(store.t('authUrlFailed') + ': ' + e.message, 'error');
}
},
confirmDeleteAccount(email) {
this.deleteTarget = email;
document.getElementById('delete_account_modal').showModal();
},
async executeDelete() {
const email = this.deleteTarget;
return await window.ErrorHandler.withLoading(async () => {
const store = Alpine.store('global');
const { response, newPassword } = await window.utils.request(
`/api/accounts/${encodeURIComponent(email)}`,
{ method: 'DELETE' },
store.webuiPassword
);
if (newPassword) store.webuiPassword = newPassword;
const data = await response.json();
if (data.status === 'ok') {
store.showToast(store.t('deletedAccount', { email }), 'success');
Alpine.store('data').fetchData();
document.getElementById('delete_account_modal').close();
this.deleteTarget = '';
} else {
throw new Error(data.error || store.t('deleteFailed'));
}
}, this, 'deleting', { errorMessage: 'Failed to delete account' });
},
async reloadAccounts() {
return await window.ErrorHandler.withLoading(async () => {
const store = Alpine.store('global');
const { response, newPassword } = await window.utils.request(
'/api/accounts/reload',
{ method: 'POST' },
store.webuiPassword
);
if (newPassword) store.webuiPassword = newPassword;
const data = await response.json();
if (data.status === 'ok') {
store.showToast(store.t('accountsReloaded'), 'success');
Alpine.store('data').fetchData();
} else {
throw new Error(data.error || store.t('reloadFailed'));
}
}, this, 'reloading', { errorMessage: 'Failed to reload accounts' });
},
/**
* Get main model quota for display
* Prioritizes flagship models (Opus > Sonnet > Flash)
* @param {Object} account - Account object with limits
* @returns {Object} { percent: number|null, model: string }
*/
getMainModelQuota(account) {
const limits = account.limits || {};
const getQuotaVal = (id) => {
const l = limits[id];
if (!l) return -1;
if (l.remainingFraction !== null) return l.remainingFraction;
if (l.resetTime) return 0; // Rate limited
return -1; // Unknown
};
const validIds = Object.keys(limits).filter(id => getQuotaVal(id) >= 0);
if (validIds.length === 0) return { percent: null, model: '-' };
const getPriority = (id) => {
const lower = id.toLowerCase();
const val = getQuotaVal(id);
const isAlive = val > 0.01; // Treat < 1% as dead for priority purposes
// Hierarchy:
// 1. High Tier (Alive)
// 2. High Tier (Dead) - to warn user
// 3. Low Tier (Alive) - fallback
// 4. Low Tier (Dead)
if (lower.includes('opus')) return isAlive ? 100 : 60;
if (lower.includes('sonnet')) return isAlive ? 90 : 55;
// Gemini 3 Pro / Ultra
if (lower.includes('gemini-3') && (lower.includes('pro') || lower.includes('ultra'))) return isAlive ? 80 : 50;
if (lower.includes('pro')) return isAlive ? 75 : 45; // Other Pro models
// Mid/Low Tier
if (lower.includes('haiku')) return isAlive ? 30 : 15;
if (lower.includes('flash')) return isAlive ? 20 : 10;
return isAlive ? 5 : 0;
};
// Sort by priority desc
validIds.sort((a, b) => getPriority(b) - getPriority(a));
const bestModel = validIds[0];
const val = getQuotaVal(bestModel);
return {
percent: Math.round(val * 100),
model: bestModel
};
}
});