feat(webui): Add Models tab and refactor model configuration

- Add standalone Models tab with real-time quota/status display
- Move model identity table from Dashboard to Models tab
- Slim down Dashboard to KPI cards and charts only
- Dashboard charts now use unfiltered data (independent of Models filters)

Settings > Models improvements:
- Remove redundant Alias column (only Mapping is functional)
- Fix column misalignment bug (empty td)
- Add column widths and hidden row opacity styling
- Single row edit constraint (only one Mapping editable at a time)
- showHiddenModels toggle now only affects Settings (not Models tab)
- Update description text to match current functionality

i18n:
- Add 'models' and 'modelsPageDesc' keys (EN/ZH)
- Add 'modelMappingHint' for Claude CLI guidance
- Update 'modelsDesc' to reflect new functionality
This commit is contained in:
Wha1eChai
2026-01-09 04:39:05 +08:00
parent a4814b8c34
commit 40a766ded6
10 changed files with 1182 additions and 832 deletions

View File

@@ -439,7 +439,7 @@ window.Components.dashboard = () => ({
padding: 10,
displayColors: true,
callbacks: {
label: function(context) {
label: function (context) {
return context.dataset.label + ': ' + context.parsed.y;
}
}
@@ -531,7 +531,8 @@ window.Components.dashboard = () => ({
this.charts.quotaDistribution.destroy();
}
const rows = Alpine.store('data').quotaRows;
// Use UNFILTERED data for global health chart
const rows = Alpine.store('data').getUnfilteredQuotaData();
// Dynamic family aggregation (supports any model family)
const familyStats = {};
@@ -580,12 +581,12 @@ window.Components.dashboard = () => ({
// Labels using translations if possible
const activeLabel = family === 'claude' ? store.t('claudeActive') :
family === 'gemini' ? store.t('geminiActive') :
`${familyName} ${store.t('activeSuffix')}`;
family === 'gemini' ? store.t('geminiActive') :
`${familyName} ${store.t('activeSuffix')}`;
const depletedLabel = family === 'claude' ? store.t('claudeEmpty') :
family === 'gemini' ? store.t('geminiEmpty') :
`${familyName} ${store.t('depleted')}`;
family === 'gemini' ? store.t('geminiEmpty') :
`${familyName} ${store.t('depleted')}`;
// Active segment
data.push(activeVal);

View File

@@ -6,10 +6,36 @@
window.Components = window.Components || {};
window.Components.modelManager = () => ({
// Track which model is currently being edited (null = none)
editingModelId: null,
init() {
// Component is ready
},
/**
* Start editing a model's mapping
* @param {string} modelId - The model to edit
*/
startEditing(modelId) {
this.editingModelId = modelId;
},
/**
* Stop editing
*/
stopEditing() {
this.editingModelId = null;
},
/**
* Check if a model is being edited
* @param {string} modelId - The model to check
*/
isEditing(modelId) {
return this.editingModelId === modelId;
},
/**
* Update model configuration with authentication
* @param {string} modelId - The model ID to update

View File

@@ -0,0 +1,58 @@
/**
* Models Component
* Displays model quota/status list
* Registers itself to window.Components for Alpine.js to consume
*/
window.Components = window.Components || {};
window.Components.models = () => ({
init() {
// Ensure data is fetched when this tab becomes active
this.$watch('$store.global.activeTab', (val) => {
if (val === 'models') {
// Trigger recompute to ensure filters are applied
this.$nextTick(() => {
Alpine.store('data').computeQuotaRows();
});
}
});
// Initial compute if already on models tab
if (this.$store.global.activeTab === 'models') {
this.$nextTick(() => {
Alpine.store('data').computeQuotaRows();
});
}
},
/**
* Update model configuration (Pin/Hide quick actions)
* @param {string} modelId - The model ID to update
* @param {object} configUpdates - Configuration updates (pinned, hidden)
*/
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: ' + e.message, 'error');
}
}
});