feat(webui): refactor settings architecture and achieve full i18n coverage

This commit is contained in:
Wha1eChai
2026-01-09 00:39:25 +08:00
parent c9c5e7d486
commit a4814b8c34
10 changed files with 711 additions and 423 deletions

View File

@@ -573,18 +573,19 @@ window.Components.dashboard = () => ({
const familyColor = familyColors[family] || familyColors['unknown'];
// Get translation keys if available, otherwise capitalize
const familyName = family.charAt(0).toUpperCase() + family.slice(1);
// Get translation keys
const store = Alpine.store('global');
const familyKey = 'family' + family.charAt(0).toUpperCase() + family.slice(1);
const familyName = store.t(familyKey);
// Labels using translations if possible
const activeLabel = family === 'claude' ? store.t('claudeActive') :
family === 'gemini' ? store.t('geminiActive') :
`${familyName} Active`;
`${familyName} ${store.t('activeSuffix')}`;
const depletedLabel = family === 'claude' ? store.t('claudeEmpty') :
family === 'gemini' ? store.t('geminiEmpty') :
`${familyName} Depleted`;
`${familyName} ${store.t('depleted')}`;
// Active segment
data.push(activeVal);

View File

@@ -0,0 +1,43 @@
/**
* Model Manager Component
* Handles model configuration (pinning, hiding, aliasing, mapping)
* Registers itself to window.Components for Alpine.js to consume
*/
window.Components = window.Components || {};
window.Components.modelManager = () => ({
init() {
// Component is ready
},
/**
* Update model configuration with authentication
* @param {string} modelId - The model ID to update
* @param {object} configUpdates - Configuration updates (pinned, hidden, alias, mapping)
*/
async updateModelConfig(modelId, configUpdates) {
const store = Alpine.store('global');
try {
const { response, newPassword } = await window.utils.request('/api/models/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ modelId, config: configUpdates })
}, store.webuiPassword);
if (newPassword) store.webuiPassword = newPassword;
if (!response.ok) {
throw new Error('Failed to update model config');
}
// Optimistic update
Alpine.store('data').modelConfig[modelId] = {
...Alpine.store('data').modelConfig[modelId],
...configUpdates
};
Alpine.store('data').computeQuotaRows();
} catch (e) {
store.showToast('Failed to update model config: ' + e.message, 'error');
}
}
});

View File

@@ -0,0 +1,229 @@
/**
* Server Config Component
* Registers itself to window.Components for Alpine.js to consume
*/
window.Components = window.Components || {};
window.Components.serverConfig = () => ({
serverConfig: {},
loading: false,
advancedExpanded: false,
debounceTimers: {}, // Store debounce timers for each config field
init() {
// Initial fetch if this is the active sub-tab
if (this.activeTab === 'server') {
this.fetchServerConfig();
}
// Watch local activeTab (from parent settings scope)
this.$watch('activeTab', (tab) => {
if (tab === 'server') {
this.fetchServerConfig();
}
});
},
async fetchServerConfig() {
const password = Alpine.store('global').webuiPassword;
try {
const { response, newPassword } = await window.utils.request('/api/config', {}, password);
if (newPassword) Alpine.store('global').webuiPassword = newPassword;
if (!response.ok) throw new Error('Failed to fetch config');
const data = await response.json();
this.serverConfig = data.config || {};
} catch (e) {
console.error('Failed to fetch server config:', e);
}
},
// Password management
passwordDialog: {
show: false,
oldPassword: '',
newPassword: '',
confirmPassword: ''
},
showPasswordDialog() {
this.passwordDialog = {
show: true,
oldPassword: '',
newPassword: '',
confirmPassword: ''
};
},
hidePasswordDialog() {
this.passwordDialog = {
show: false,
oldPassword: '',
newPassword: '',
confirmPassword: ''
};
},
async changePassword() {
const store = Alpine.store('global');
const { oldPassword, newPassword, confirmPassword } = this.passwordDialog;
if (newPassword !== confirmPassword) {
store.showToast(store.t('passwordsNotMatch'), 'error');
return;
}
if (newPassword.length < 6) {
store.showToast(store.t('passwordTooShort'), 'error');
return;
}
try {
const { response } = await window.utils.request('/api/config/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ oldPassword, newPassword })
}, store.webuiPassword);
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to change password');
}
// Update stored password
store.webuiPassword = newPassword;
store.showToast('Password changed successfully', 'success');
this.hidePasswordDialog();
} catch (e) {
store.showToast('Failed to change password: ' + e.message, 'error');
}
},
// Toggle Debug Mode with instant save
async toggleDebug(enabled) {
const store = Alpine.store('global');
// Optimistic update
const previousValue = this.serverConfig.debug;
this.serverConfig.debug = enabled;
try {
const { response, newPassword } = await window.utils.request('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ debug: enabled })
}, store.webuiPassword);
if (newPassword) store.webuiPassword = newPassword;
const data = await response.json();
if (data.status === 'ok') {
const status = enabled ? 'enabled' : 'disabled';
store.showToast(`Debug mode ${status}`, 'success');
await this.fetchServerConfig(); // Confirm server state
} else {
throw new Error(data.error || 'Failed to update debug mode');
}
} catch (e) {
// Rollback on error
this.serverConfig.debug = previousValue;
store.showToast('Failed to update debug mode: ' + e.message, 'error');
}
},
// Toggle Token Cache with instant save
async toggleTokenCache(enabled) {
const store = Alpine.store('global');
// Optimistic update
const previousValue = this.serverConfig.persistTokenCache;
this.serverConfig.persistTokenCache = enabled;
try {
const { response, newPassword } = await window.utils.request('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ persistTokenCache: enabled })
}, store.webuiPassword);
if (newPassword) store.webuiPassword = newPassword;
const data = await response.json();
if (data.status === 'ok') {
const status = enabled ? 'enabled' : 'disabled';
store.showToast(`Token cache ${status}`, 'success');
await this.fetchServerConfig(); // Confirm server state
} else {
throw new Error(data.error || 'Failed to update token cache');
}
} catch (e) {
// Rollback on error
this.serverConfig.persistTokenCache = previousValue;
store.showToast('Failed to update token cache: ' + e.message, 'error');
}
},
// Generic debounced save method for numeric configs
async saveConfigField(fieldName, value, displayName) {
const store = Alpine.store('global');
// Clear existing timer for this field
if (this.debounceTimers[fieldName]) {
clearTimeout(this.debounceTimers[fieldName]);
}
// Optimistic update
const previousValue = this.serverConfig[fieldName];
this.serverConfig[fieldName] = parseInt(value);
// Set new timer
this.debounceTimers[fieldName] = setTimeout(async () => {
try {
const payload = {};
payload[fieldName] = parseInt(value);
const { response, newPassword } = await window.utils.request('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}, store.webuiPassword);
if (newPassword) store.webuiPassword = newPassword;
const data = await response.json();
if (data.status === 'ok') {
store.showToast(`${displayName} updated to ${value}`, 'success');
await this.fetchServerConfig(); // Confirm server state
} else {
throw new Error(data.error || `Failed to update ${displayName}`);
}
} catch (e) {
// Rollback on error
this.serverConfig[fieldName] = previousValue;
store.showToast(`Failed to update ${displayName}: ` + e.message, 'error');
}
}, 500); // 500ms debounce
},
// Individual toggle methods for each Advanced Tuning field
toggleMaxRetries(value) {
this.saveConfigField('maxRetries', value, 'Max Retries');
},
toggleRetryBaseMs(value) {
this.saveConfigField('retryBaseMs', value, 'Retry Base Delay');
},
toggleRetryMaxMs(value) {
this.saveConfigField('retryMaxMs', value, 'Retry Max Delay');
},
toggleDefaultCooldownMs(value) {
this.saveConfigField('defaultCooldownMs', value, 'Default Cooldown');
},
toggleMaxWaitBeforeErrorMs(value) {
this.saveConfigField('maxWaitBeforeErrorMs', value, 'Max Wait Threshold');
}
});