feat(webui): refactor settings architecture and achieve full i18n coverage
This commit is contained in:
@@ -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);
|
||||
|
||||
43
public/js/components/model-manager.js
Normal file
43
public/js/components/model-manager.js
Normal 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');
|
||||
}
|
||||
}
|
||||
});
|
||||
229
public/js/components/server-config.js
Normal file
229
public/js/components/server-config.js
Normal 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');
|
||||
}
|
||||
});
|
||||
@@ -167,6 +167,9 @@ document.addEventListener('alpine:init', () => {
|
||||
configSaved: "Configuration Saved",
|
||||
enterPassword: "Enter Web UI Password:",
|
||||
ready: "READY",
|
||||
depleted: "Depleted",
|
||||
timeH: "H",
|
||||
timeM: "M",
|
||||
familyClaude: "Claude",
|
||||
familyGemini: "Gemini",
|
||||
familyOther: "Other",
|
||||
@@ -176,6 +179,43 @@ document.addEventListener('alpine:init', () => {
|
||||
logLevelSuccess: "SUCCESS",
|
||||
logLevelWarn: "WARN",
|
||||
logLevelError: "ERR",
|
||||
totalColon: "Total:",
|
||||
todayColon: "Today:",
|
||||
hour1Colon: "1H:",
|
||||
smart: "Smart",
|
||||
smartTitle: "Select Top 5 most used models (24h)",
|
||||
activeCount: "{count} Active",
|
||||
allCaps: "ALL",
|
||||
claudeCaps: "CLAUDE",
|
||||
geminiCaps: "GEMINI",
|
||||
modelMapping: "Mapping (Target Model ID)",
|
||||
systemInfo: "System Information",
|
||||
refresh: "Refresh",
|
||||
runtimeConfig: "Runtime Configuration",
|
||||
debugDesc: "Enable detailed logging (See Logs tab)",
|
||||
networkRetry: "Network Retry Settings",
|
||||
maxRetries: "Max Retries",
|
||||
retryBaseDelay: "Retry Base Delay (ms)",
|
||||
retryMaxDelay: "Retry Max Delay (ms)",
|
||||
persistTokenCache: "Persist Token Cache",
|
||||
persistTokenDesc: "Save OAuth tokens to disk for faster restarts",
|
||||
rateLimiting: "Account Rate Limiting & Timeouts",
|
||||
defaultCooldown: "Default Cooldown Time",
|
||||
maxWaitThreshold: "Max Wait Threshold (Sticky)",
|
||||
maxWaitDesc: "Maximum time to wait for a sticky account to reset before failing or switching.",
|
||||
saveConfigServer: "Save Configuration",
|
||||
serverRestartAlert: "Some changes may require server restart. Config is saved to {path}",
|
||||
changePassword: "Change WebUI Password",
|
||||
changePasswordDesc: "Update the password for accessing this dashboard",
|
||||
currentPassword: "Current Password",
|
||||
newPassword: "New Password",
|
||||
confirmNewPassword: "Confirm New Password",
|
||||
passwordEmptyDesc: "Leave empty if no password set",
|
||||
passwordLengthDesc: "At least 6 characters",
|
||||
passwordConfirmDesc: "Re-enter new password",
|
||||
cancel: "Cancel",
|
||||
passwordsNotMatch: "Passwords do not match",
|
||||
passwordTooShort: "Password must be at least 6 characters",
|
||||
},
|
||||
zh: {
|
||||
dashboard: "仪表盘",
|
||||
@@ -332,6 +372,9 @@ document.addEventListener('alpine:init', () => {
|
||||
configSaved: "配置已保存",
|
||||
enterPassword: "请输入 Web UI 密码:",
|
||||
ready: "就绪",
|
||||
depleted: "已耗尽",
|
||||
timeH: "时",
|
||||
timeM: "分",
|
||||
familyClaude: "Claude 系列",
|
||||
familyGemini: "Gemini 系列",
|
||||
familyOther: "其他系列",
|
||||
@@ -341,6 +384,43 @@ document.addEventListener('alpine:init', () => {
|
||||
logLevelSuccess: "成功",
|
||||
logLevelWarn: "警告",
|
||||
logLevelError: "错误",
|
||||
totalColon: "总计:",
|
||||
todayColon: "今日:",
|
||||
hour1Colon: "1小时:",
|
||||
smart: "智能选择",
|
||||
smartTitle: "自动选择过去 24 小时最常用的 5 个模型",
|
||||
activeCount: "{count} 活跃",
|
||||
allCaps: "全部",
|
||||
claudeCaps: "CLAUDE",
|
||||
geminiCaps: "GEMINI",
|
||||
modelMapping: "映射 (目标模型 ID)",
|
||||
systemInfo: "系统信息",
|
||||
refresh: "刷新",
|
||||
runtimeConfig: "运行时配置",
|
||||
debugDesc: "启用详细日志记录 (见运行日志)",
|
||||
networkRetry: "网络重试设置",
|
||||
maxRetries: "最大重试次数",
|
||||
retryBaseDelay: "重试基础延迟 (毫秒)",
|
||||
retryMaxDelay: "重试最大延迟 (毫秒)",
|
||||
persistTokenCache: "持久化令牌缓存",
|
||||
persistTokenDesc: "将 OAuth 令牌保存到磁盘以实现快速重启",
|
||||
rateLimiting: "账号限流与超时",
|
||||
defaultCooldown: "默认冷却时间",
|
||||
maxWaitThreshold: "最大等待阈值 (粘性会话)",
|
||||
maxWaitDesc: "粘性账号在失败或切换前等待重置的最长时间。",
|
||||
saveConfigServer: "保存配置",
|
||||
serverRestartAlert: "部分更改可能需要重启服务器。配置已保存至 {path}",
|
||||
changePassword: "修改 WebUI 密码",
|
||||
changePasswordDesc: "更新访问此仪表盘的密码",
|
||||
currentPassword: "当前密码",
|
||||
newPassword: "新密码",
|
||||
confirmNewPassword: "确认新密码",
|
||||
passwordEmptyDesc: "如果未设置密码请留空",
|
||||
passwordLengthDesc: "至少 6 个字符",
|
||||
passwordConfirmDesc: "请再次输入新密码",
|
||||
cancel: "取消",
|
||||
passwordsNotMatch: "密码不匹配",
|
||||
passwordTooShort: "密码至少需要 6 个字符",
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -37,8 +37,12 @@ window.utils = {
|
||||
if (diff <= 0) return store ? store.t('ready') : 'READY';
|
||||
const mins = Math.floor(diff / 60000);
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs > 0) return `${hrs}H ${mins % 60}M`;
|
||||
return `${mins}M`;
|
||||
|
||||
const hSuffix = store ? store.t('timeH') : 'H';
|
||||
const mSuffix = store ? store.t('timeM') : 'M';
|
||||
|
||||
if (hrs > 0) return `${hrs}${hSuffix} ${mins % 60}${mSuffix}`;
|
||||
return `${mins}${mSuffix}`;
|
||||
},
|
||||
|
||||
getThemeColor(name) {
|
||||
|
||||
Reference in New Issue
Block a user