@@ -295,6 +318,7 @@
+
diff --git a/public/js/components/dashboard.js b/public/js/components/dashboard.js
index 6f71e5d..3ef00df 100644
--- a/public/js/components/dashboard.js
+++ b/public/js/components/dashboard.js
@@ -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);
diff --git a/public/js/components/model-manager.js b/public/js/components/model-manager.js
index 69554f4..3ec866a 100644
--- a/public/js/components/model-manager.js
+++ b/public/js/components/model-manager.js
@@ -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
diff --git a/public/js/components/models.js b/public/js/components/models.js
new file mode 100644
index 0000000..84d4581
--- /dev/null
+++ b/public/js/components/models.js
@@ -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');
+ }
+ }
+});
diff --git a/public/js/data-store.js b/public/js/data-store.js
index 867c937..36414de 100644
--- a/public/js/data-store.js
+++ b/public/js/data-store.js
@@ -69,34 +69,32 @@ document.addEventListener('alpine:init', () => {
computeQuotaRows() {
const models = this.models || [];
const rows = [];
- const showExhausted = Alpine.store('settings')?.showExhausted ?? true; // Need settings store
- // Temporary debug flag or settings flag to show hidden models
- const showHidden = Alpine.store('settings')?.showHiddenModels ?? false;
+ const showExhausted = Alpine.store('settings')?.showExhausted ?? true;
models.forEach(modelId => {
// Config
const config = this.modelConfig[modelId] || {};
const family = this.getModelFamily(modelId);
- // Smart Visibility Logic:
- // 1. If explicit config exists, use it.
- // 2. If no config, default 'unknown' families to HIDDEN to prevent clutter.
- // 3. Known families (Claude/Gemini) default to VISIBLE.
+ // Visibility Logic for Models Tab (quotaRows):
+ // 1. If explicitly hidden via config, always hide
+ // 2. If no config, default 'unknown' families to HIDDEN
+ // 3. Known families (Claude/Gemini) default to VISIBLE
+ // Note: showHiddenModels toggle is for Settings page only, NOT here
let isHidden = config.hidden;
if (isHidden === undefined) {
isHidden = (family === 'other' || family === 'unknown');
}
- // Skip hidden models unless "Show Hidden" is enabled
- if (isHidden && !showHidden) return;
+ // Models Tab: ALWAYS hide hidden models (no toggle check)
+ if (isHidden) return;
// Filters
if (this.filters.family !== 'all' && this.filters.family !== family) return;
if (this.filters.search) {
const searchLower = this.filters.search.toLowerCase();
- const aliasMatch = config.alias && config.alias.toLowerCase().includes(searchLower);
const idMatch = modelId.toLowerCase().includes(searchLower);
- if (!aliasMatch && !idMatch) return;
+ if (!idMatch) return;
}
// Data Collection
@@ -138,7 +136,7 @@ document.addEventListener('alpine:init', () => {
rows.push({
modelId,
- displayName: config.alias || modelId, // Use alias if available
+ displayName: modelId, // Simplified: no longer using alias
family,
minQuota,
avgQuota, // Added Average Quota
@@ -165,6 +163,43 @@ document.addEventListener('alpine:init', () => {
if (lower.includes('claude')) return 'claude';
if (lower.includes('gemini')) return 'gemini';
return 'other';
+ },
+
+ /**
+ * Get quota data without filters applied (for Dashboard global charts)
+ * Returns array of { modelId, family, quotaInfo: [{pct}] }
+ */
+ getUnfilteredQuotaData() {
+ const models = this.models || [];
+ const rows = [];
+ const showHidden = Alpine.store('settings')?.showHiddenModels ?? false;
+
+ models.forEach(modelId => {
+ const config = this.modelConfig[modelId] || {};
+ const family = this.getModelFamily(modelId);
+
+ // Smart visibility (same logic as computeQuotaRows)
+ let isHidden = config.hidden;
+ if (isHidden === undefined) {
+ isHidden = (family === 'other' || family === 'unknown');
+ }
+ if (isHidden && !showHidden) return;
+
+ const quotaInfo = [];
+ // Use ALL accounts (no account filter)
+ this.accounts.forEach(acc => {
+ const limit = acc.limits?.[modelId];
+ if (!limit) return;
+ const pct = limit.remainingFraction !== null ? Math.round(limit.remainingFraction * 100) : 0;
+ quotaInfo.push({ pct });
+ });
+
+ if (quotaInfo.length === 0) return;
+
+ rows.push({ modelId, family, quotaInfo });
+ });
+
+ return rows;
}
});
});
diff --git a/public/js/store.js b/public/js/store.js
index 81f008c..24a99a1 100644
--- a/public/js/store.js
+++ b/public/js/store.js
@@ -15,6 +15,7 @@ document.addEventListener('alpine:init', () => {
translations: {
en: {
dashboard: "Dashboard",
+ models: "Models",
accounts: "Accounts",
logs: "Logs",
settings: "Settings",
@@ -77,14 +78,16 @@ document.addEventListener('alpine:init', () => {
noSignal: "NO SIGNAL DETECTED",
establishingUplink: "ESTABLISHING UPLINK...",
// Settings - Models
- modelsDesc: "Manage visibility and ordering of models in the dashboard.",
+ modelsDesc: "Configure model visibility, pinning, and request redirection.",
+ modelsPageDesc: "Real-time quota and status for all available models.",
showHidden: "Show Hidden Models",
modelId: "Model ID",
- alias: "Alias",
actions: "Actions",
pinToTop: "Pin to top",
toggleVisibility: "Toggle Visibility",
noModels: "NO MODELS DETECTED",
+ modelMappingHint: "Server-side model redirection. Claude Code users: see 'Claude CLI' tab for easier setup.",
+ modelMapping: "Mapping (Target Model)",
// Settings - Claude
proxyConnection: "Proxy Connection",
modelSelection: "Model Selection",
@@ -219,6 +222,7 @@ document.addEventListener('alpine:init', () => {
},
zh: {
dashboard: "仪表盘",
+ models: "模型列表",
accounts: "账号管理",
logs: "运行日志",
settings: "系统设置",
@@ -282,14 +286,16 @@ document.addEventListener('alpine:init', () => {
noSignal: "无信号连接",
establishingUplink: "正在建立上行链路...",
// Settings - Models
- modelsDesc: "管理仪表盘中模型的可见性和排序。",
+ modelsDesc: "配置模型的可见性、置顶和请求重定向。",
+ modelsPageDesc: "所有可用模型的实时配额和状态。",
showHidden: "显示隐藏模型",
modelId: "模型 ID",
- alias: "别名",
actions: "操作",
pinToTop: "置顶",
toggleVisibility: "切换可见性",
noModels: "未检测到模型",
+ modelMappingHint: "服务端模型重定向功能。Claude Code 用户请使用 'Claude CLI' 标签页以便捷配置。",
+ modelMapping: "映射 (目标模型)",
// Settings - Claude
proxyConnection: "代理连接",
modelSelection: "模型选择",
diff --git a/public/views/dashboard.html b/public/views/dashboard.html
index 24601ff..6522062 100644
--- a/public/views/dashboard.html
+++ b/public/views/dashboard.html
@@ -5,8 +5,7 @@
class="stat bg-space-900/40 border border-space-border/30 rounded-lg p-4 hover:border-space-border/60 transition-colors group relative">
-