Files
antigravity-claude-proxy/public/js/components/account-manager.js
jgor20 eb4cbc0ce5 refactor(components): enhance model identification precision
Switch from substring includes to regex word boundary tests for model names like opus, sonnet, and gemini variants, improving accuracy in the account manager's prioritization logic.
2026-01-11 14:46:41 +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 (/\bopus\b/.test(lower)) return isAlive ? 100 : 60;
if (/\bsonnet\b/.test(lower)) return isAlive ? 90 : 55;
// Gemini 3 Pro / Ultra
if (/\bgemini-3\b/.test(lower) && (/\bpro\b/.test(lower) || /\bultra\b/.test(lower))) return isAlive ? 80 : 50;
if (/\bpro\b/.test(lower)) return isAlive ? 75 : 45; // Other Pro models
// Mid/Low Tier
if (/\bhaiku\b/.test(lower)) return isAlive ? 30 : 15;
if (/\bflash\b/.test(lower)) 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
};
}
});