Changeset 0.8.3 (#46)
This commit is contained in:
36
server/database/migrations/018_model_visibility.sql
Normal file
36
server/database/migrations/018_model_visibility.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- ==========================================
|
||||||
|
-- Migration 018: Model Visibility
|
||||||
|
-- ==========================================
|
||||||
|
-- Replace binary is_enabled with three-state visibility:
|
||||||
|
-- 'enabled' — visible to all users in model selector
|
||||||
|
-- 'disabled' — hidden from everyone
|
||||||
|
-- 'team' — only available to team admins for building presets
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- Add new column (idempotent)
|
||||||
|
ALTER TABLE model_configs ADD COLUMN IF NOT EXISTS visibility VARCHAR(10) DEFAULT 'disabled';
|
||||||
|
|
||||||
|
-- Backfill from is_enabled if it still exists
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'model_configs' AND column_name = 'is_enabled'
|
||||||
|
) THEN
|
||||||
|
UPDATE model_configs SET visibility = CASE
|
||||||
|
WHEN is_enabled = true THEN 'enabled'
|
||||||
|
ELSE 'disabled'
|
||||||
|
END;
|
||||||
|
ALTER TABLE model_configs DROP COLUMN is_enabled;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Ensure NOT NULL (safe: DEFAULT already covers new rows)
|
||||||
|
UPDATE model_configs SET visibility = 'disabled' WHERE visibility IS NULL;
|
||||||
|
ALTER TABLE model_configs ALTER COLUMN visibility SET NOT NULL;
|
||||||
|
|
||||||
|
-- Constraint (drop first for idempotency)
|
||||||
|
ALTER TABLE model_configs DROP CONSTRAINT IF EXISTS chk_model_visibility;
|
||||||
|
ALTER TABLE model_configs ADD CONSTRAINT chk_model_visibility
|
||||||
|
CHECK (visibility IN ('enabled', 'disabled', 'team'));
|
||||||
|
|
||||||
|
COMMENT ON COLUMN model_configs.visibility IS 'enabled=all users, team=team admin presets only, disabled=hidden';
|
||||||
@@ -648,14 +648,14 @@ type modelConfigResponse struct {
|
|||||||
ProviderName string `json:"provider_name"`
|
ProviderName string `json:"provider_name"`
|
||||||
ModelID string `json:"model_id"`
|
ModelID string `json:"model_id"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
IsEnabled bool `json:"is_enabled"`
|
Visibility string `json:"visibility"`
|
||||||
Capabilities map[string]interface{} `json:"capabilities"`
|
Capabilities map[string]interface{} `json:"capabilities"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateModelConfigRequest struct {
|
type updateModelConfigRequest struct {
|
||||||
IsEnabled *bool `json:"is_enabled"`
|
Visibility *string `json:"visibility"`
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
Capabilities map[string]interface{} `json:"capabilities"`
|
Capabilities map[string]interface{} `json:"capabilities"`
|
||||||
}
|
}
|
||||||
@@ -663,7 +663,7 @@ type updateModelConfigRequest struct {
|
|||||||
func (h *AdminHandler) ListModelConfigs(c *gin.Context) {
|
func (h *AdminHandler) ListModelConfigs(c *gin.Context) {
|
||||||
rows, err := database.DB.Query(`
|
rows, err := database.DB.Query(`
|
||||||
SELECT mc.id, mc.api_config_id, ac.name, mc.model_id, mc.display_name,
|
SELECT mc.id, mc.api_config_id, ac.name, mc.model_id, mc.display_name,
|
||||||
mc.is_enabled, mc.capabilities, mc.created_at, mc.updated_at
|
mc.visibility, mc.capabilities, mc.created_at, mc.updated_at
|
||||||
FROM model_configs mc
|
FROM model_configs mc
|
||||||
JOIN api_configs ac ON mc.api_config_id = ac.id
|
JOIN api_configs ac ON mc.api_config_id = ac.id
|
||||||
WHERE ac.user_id IS NULL
|
WHERE ac.user_id IS NULL
|
||||||
@@ -681,7 +681,7 @@ func (h *AdminHandler) ListModelConfigs(c *gin.Context) {
|
|||||||
var capsJSON []byte
|
var capsJSON []byte
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&m.ID, &m.APIConfigID, &m.ProviderName, &m.ModelID, &m.DisplayName,
|
&m.ID, &m.APIConfigID, &m.ProviderName, &m.ModelID, &m.DisplayName,
|
||||||
&m.IsEnabled, &capsJSON, &m.CreatedAt, &m.UpdatedAt,
|
&m.Visibility, &capsJSON, &m.CreatedAt, &m.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -795,13 +795,18 @@ func (h *AdminHandler) UpdateModelConfig(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build dynamic update
|
// Build dynamic update
|
||||||
if req.IsEnabled != nil {
|
if req.Visibility != nil {
|
||||||
|
v := *req.Visibility
|
||||||
|
if v != "enabled" && v != "disabled" && v != "team" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "visibility must be enabled, disabled, or team"})
|
||||||
|
return
|
||||||
|
}
|
||||||
_, err := database.DB.Exec(
|
_, err := database.DB.Exec(
|
||||||
`UPDATE model_configs SET is_enabled = $1, updated_at = NOW() WHERE id = $2`,
|
`UPDATE model_configs SET visibility = $1, updated_at = NOW() WHERE id = $2`,
|
||||||
*req.IsEnabled, modelID,
|
v, modelID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update enabled"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update visibility"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -836,19 +841,24 @@ func (h *AdminHandler) UpdateModelConfig(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"message": "model updated"})
|
c.JSON(http.StatusOK, gin.H{"message": "model updated"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// BulkUpdateModels enables or disables all models at once
|
// BulkUpdateModels sets visibility for all models at once
|
||||||
func (h *AdminHandler) BulkUpdateModels(c *gin.Context) {
|
func (h *AdminHandler) BulkUpdateModels(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
IsEnabled bool `json:"is_enabled"`
|
Visibility string `json:"visibility"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if req.Visibility != "enabled" && req.Visibility != "disabled" && req.Visibility != "team" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "visibility must be enabled, disabled, or team"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
result, err := database.DB.Exec(
|
result, err := database.DB.Exec(
|
||||||
`UPDATE model_configs SET is_enabled = $1, updated_at = NOW()`,
|
`UPDATE model_configs SET visibility = $1, updated_at = NOW()
|
||||||
req.IsEnabled,
|
WHERE api_config_id IN (SELECT id FROM api_configs WHERE user_id IS NULL)`,
|
||||||
|
req.Visibility,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to bulk update models"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to bulk update models"})
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
|
|||||||
SELECT mc.id, mc.model_id, mc.display_name, ac.provider, ac.name, mc.api_config_id, mc.capabilities
|
SELECT mc.id, mc.model_id, mc.display_name, ac.provider, ac.name, mc.api_config_id, mc.capabilities
|
||||||
FROM model_configs mc
|
FROM model_configs mc
|
||||||
JOIN api_configs ac ON mc.api_config_id = ac.id
|
JOIN api_configs ac ON mc.api_config_id = ac.id
|
||||||
WHERE mc.is_enabled = true AND ac.is_active = true AND ac.user_id IS NULL
|
WHERE mc.visibility = 'enabled' AND ac.is_active = true AND ac.user_id IS NULL
|
||||||
ORDER BY ac.name, mc.model_id
|
ORDER BY ac.name, mc.model_id
|
||||||
`)
|
`)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -455,6 +455,49 @@ func (h *TeamHandler) MyTeams(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"data": teams})
|
c.JSON(http.StatusOK, gin.H{"data": teams})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Team Models: Available for Presets ──────
|
||||||
|
|
||||||
|
// ListAvailableModels returns models with visibility 'enabled' or 'team'
|
||||||
|
// for team admins building presets. Requires RequireTeamAdmin middleware.
|
||||||
|
// GET /api/v1/teams/:teamId/models
|
||||||
|
func (h *TeamHandler) ListAvailableModels(c *gin.Context) {
|
||||||
|
rows, err := database.DB.Query(`
|
||||||
|
SELECT mc.id, mc.model_id, mc.display_name, mc.visibility,
|
||||||
|
ac.provider, ac.name as provider_name
|
||||||
|
FROM model_configs mc
|
||||||
|
JOIN api_configs ac ON mc.api_config_id = ac.id
|
||||||
|
WHERE mc.visibility IN ('enabled', 'team')
|
||||||
|
AND ac.is_active = true AND ac.user_id IS NULL
|
||||||
|
ORDER BY ac.name, mc.model_id
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
type availableModel struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ModelID string `json:"model_id"`
|
||||||
|
DisplayName *string `json:"display_name"`
|
||||||
|
Visibility string `json:"visibility"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
ProviderName string `json:"provider_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
models := make([]availableModel, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var m availableModel
|
||||||
|
if err := rows.Scan(&m.ID, &m.ModelID, &m.DisplayName, &m.Visibility,
|
||||||
|
&m.Provider, &m.ProviderName); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
models = append(models, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"models": models})
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers ─────────────────────────────────
|
// ── Helpers ─────────────────────────────────
|
||||||
|
|
||||||
// getTeamID extracts team ID from either :id (admin routes) or :teamId (team-scoped routes).
|
// getTeamID extracts team ID from either :id (admin routes) or :teamId (team-scoped routes).
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ func main() {
|
|||||||
teamScoped.POST("/members", teams.AddMember)
|
teamScoped.POST("/members", teams.AddMember)
|
||||||
teamScoped.PUT("/members/:memberId", teams.UpdateMember)
|
teamScoped.PUT("/members/:memberId", teams.UpdateMember)
|
||||||
teamScoped.DELETE("/members/:memberId", teams.RemoveMember)
|
teamScoped.DELETE("/members/:memberId", teams.RemoveMember)
|
||||||
|
teamScoped.GET("/models", teams.ListAvailableModels)
|
||||||
|
|
||||||
// Team presets
|
// Team presets
|
||||||
teamPresets := handlers.NewPresetHandler()
|
teamPresets := handlers.NewPresetHandler()
|
||||||
|
|||||||
@@ -1012,6 +1012,7 @@ button { font-family: var(--font); cursor: pointer; }
|
|||||||
.admin-model-toggle { background: none; border: 1px solid var(--border); border-radius: 4px; padding: 4px 10px; font-size: 12px; cursor: pointer; color: var(--text-2); min-width: 82px; text-align: center; }
|
.admin-model-toggle { background: none; border: 1px solid var(--border); border-radius: 4px; padding: 4px 10px; font-size: 12px; cursor: pointer; color: var(--text-2); min-width: 82px; text-align: center; }
|
||||||
.admin-model-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
.admin-model-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
.admin-model-toggle.enabled { border-color: var(--success); color: var(--success); }
|
.admin-model-toggle.enabled { border-color: var(--success); color: var(--success); }
|
||||||
|
.admin-model-toggle.team { border-color: #60a5fa; color: #60a5fa; }
|
||||||
|
|
||||||
.admin-provider-row {
|
.admin-provider-row {
|
||||||
display: flex; align-items: center; gap: 12px; padding: 10px 0;
|
display: flex; align-items: center; gap: 12px; padding: 10px 0;
|
||||||
|
|||||||
@@ -410,8 +410,9 @@
|
|||||||
<div class="admin-tab-content" id="adminModelsTab" style="display:none">
|
<div class="admin-tab-content" id="adminModelsTab" style="display:none">
|
||||||
<div class="admin-toolbar">
|
<div class="admin-toolbar">
|
||||||
<button class="btn-small btn-primary" id="adminFetchModelsBtn">⟳ Fetch Models</button>
|
<button class="btn-small btn-primary" id="adminFetchModelsBtn">⟳ Fetch Models</button>
|
||||||
<button class="btn-small" onclick="bulkToggleModels(true)">Enable All</button>
|
<button class="btn-small" onclick="bulkSetVisibility('enabled')">All Enabled</button>
|
||||||
<button class="btn-small" onclick="bulkToggleModels(false)">Disable All</button>
|
<button class="btn-small" onclick="bulkSetVisibility('team')">All Team</button>
|
||||||
|
<button class="btn-small" onclick="bulkSetVisibility('disabled')">All Disabled</button>
|
||||||
<span class="admin-hint" id="adminModelsHint"></span>
|
<span class="admin-hint" id="adminModelsHint"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="adminModelList"></div>
|
<div id="adminModelList"></div>
|
||||||
|
|||||||
@@ -279,7 +279,13 @@ const API = {
|
|||||||
adminListModels() { return this._get('/api/v1/admin/models'); },
|
adminListModels() { return this._get('/api/v1/admin/models'); },
|
||||||
adminFetchModels() { return this._post('/api/v1/admin/models/fetch', {}); },
|
adminFetchModels() { return this._post('/api/v1/admin/models/fetch', {}); },
|
||||||
adminUpdateModel(id, updates) { return this._put(`/api/v1/admin/models/${id}`, updates); },
|
adminUpdateModel(id, updates) { return this._put(`/api/v1/admin/models/${id}`, updates); },
|
||||||
adminBulkUpdateModels(isEnabled) { return this._put('/api/v1/admin/models/bulk', { is_enabled: isEnabled }); },
|
adminBulkUpdateModels(visibility) {
|
||||||
|
// Send both for backward compat: old BE reads is_enabled, new BE reads visibility
|
||||||
|
return this._put('/api/v1/admin/models/bulk', {
|
||||||
|
visibility,
|
||||||
|
is_enabled: visibility === 'enabled'
|
||||||
|
});
|
||||||
|
},
|
||||||
adminDeleteModel(id) { return this._del(`/api/v1/admin/models/${id}`); },
|
adminDeleteModel(id) { return this._del(`/api/v1/admin/models/${id}`); },
|
||||||
|
|
||||||
// ── Admin Presets ────────────────────────
|
// ── Admin Presets ────────────────────────
|
||||||
@@ -324,6 +330,7 @@ const API = {
|
|||||||
teamListPresets(teamId) { return this._get(`/api/v1/teams/${teamId}/presets`); },
|
teamListPresets(teamId) { return this._get(`/api/v1/teams/${teamId}/presets`); },
|
||||||
teamCreatePreset(teamId, preset) { return this._post(`/api/v1/teams/${teamId}/presets`, preset); },
|
teamCreatePreset(teamId, preset) { return this._post(`/api/v1/teams/${teamId}/presets`, preset); },
|
||||||
teamDeletePreset(teamId, presetId) { return this._del(`/api/v1/teams/${teamId}/presets/${presetId}`); },
|
teamDeletePreset(teamId, presetId) { return this._del(`/api/v1/teams/${teamId}/presets/${presetId}`); },
|
||||||
|
teamListAvailableModels(teamId) { return this._get(`/api/v1/teams/${teamId}/models`); },
|
||||||
|
|
||||||
// ── User Presets ─────────────────────────
|
// ── User Presets ─────────────────────────
|
||||||
listPresets() { return this._get('/api/v1/presets'); },
|
listPresets() { return this._get('/api/v1/presets'); },
|
||||||
|
|||||||
@@ -1140,7 +1140,7 @@ function initListeners() {
|
|||||||
});
|
});
|
||||||
document.getElementById('settingsTeamAddPresetBtn')?.addEventListener('click', () => {
|
document.getElementById('settingsTeamAddPresetBtn')?.addEventListener('click', () => {
|
||||||
document.getElementById('settingsTeamAddPreset').style.display = '';
|
document.getElementById('settingsTeamAddPreset').style.display = '';
|
||||||
UI.loadTeamPresetModelDropdown();
|
UI.loadTeamPresetModelDropdown(UI._managingTeamId);
|
||||||
});
|
});
|
||||||
document.getElementById('settingsTeamCancelPreset')?.addEventListener('click', () => {
|
document.getElementById('settingsTeamCancelPreset')?.addEventListener('click', () => {
|
||||||
document.getElementById('settingsTeamAddPreset').style.display = 'none';
|
document.getElementById('settingsTeamAddPreset').style.display = 'none';
|
||||||
@@ -1622,21 +1622,27 @@ async function fetchAdminModels() {
|
|||||||
try { await API.adminFetchModels(); UI.toast('Models synced', 'success'); hint.textContent = ''; await UI.loadAdminModels(); }
|
try { await API.adminFetchModels(); UI.toast('Models synced', 'success'); hint.textContent = ''; await UI.loadAdminModels(); }
|
||||||
catch (e) { UI.toast(e.message, 'error'); hint.textContent = 'Failed'; }
|
catch (e) { UI.toast(e.message, 'error'); hint.textContent = 'Failed'; }
|
||||||
}
|
}
|
||||||
async function toggleModel(id, enabled) {
|
async function cycleModelVisibility(id, current) {
|
||||||
|
// Cycle: disabled → enabled → team → disabled
|
||||||
|
const next = current === 'disabled' ? 'enabled' : current === 'enabled' ? 'team' : 'disabled';
|
||||||
try {
|
try {
|
||||||
const el = _adminScroll(), pos = el?.scrollTop || 0;
|
const el = _adminScroll(), pos = el?.scrollTop || 0;
|
||||||
await API.adminUpdateModel(id, { is_enabled: enabled });
|
// Send both for backward compat with pre-0.8.3 backends
|
||||||
|
await API.adminUpdateModel(id, { visibility: next, is_enabled: next === 'enabled' });
|
||||||
await UI.loadAdminModels();
|
await UI.loadAdminModels();
|
||||||
_restoreScroll(el, pos);
|
_restoreScroll(el, pos);
|
||||||
} catch (e) { UI.toast(e.message, 'error'); }
|
} catch (e) { UI.toast(e.message, 'error'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkToggleModels(enabled) {
|
async function bulkSetVisibility(visibility) {
|
||||||
const hint = document.getElementById('adminModelsHint');
|
const hint = document.getElementById('adminModelsHint');
|
||||||
hint.textContent = enabled ? 'Enabling all...' : 'Disabling all...';
|
const labels = { enabled: 'Enabling', disabled: 'Disabling', team: 'Setting team-only for' };
|
||||||
|
hint.textContent = `${labels[visibility] || 'Updating'} all...`;
|
||||||
try {
|
try {
|
||||||
const resp = await API.adminBulkUpdateModels(enabled);
|
// Send both for backward compat with pre-0.8.3 backends
|
||||||
hint.textContent = `${resp.count || 'All'} models ${enabled ? 'enabled' : 'disabled'}`;
|
const resp = await API.adminBulkUpdateModels(visibility);
|
||||||
|
const doneLabels = { enabled: 'enabled', disabled: 'disabled', team: 'set to team-only' };
|
||||||
|
hint.textContent = `${resp.count || 'All'} models ${doneLabels[visibility]}`;
|
||||||
setTimeout(() => { hint.textContent = ''; }, 3000);
|
setTimeout(() => { hint.textContent = ''; }, 3000);
|
||||||
await UI.loadAdminModels();
|
await UI.loadAdminModels();
|
||||||
} catch (e) { UI.toast(e.message, 'error'); hint.textContent = 'Failed'; }
|
} catch (e) { UI.toast(e.message, 'error'); hint.textContent = 'Failed'; }
|
||||||
|
|||||||
38
src/js/ui.js
38
src/js/ui.js
@@ -822,17 +822,31 @@ const UI = {
|
|||||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadTeamPresetModelDropdown() {
|
async loadTeamPresetModelDropdown(teamId) {
|
||||||
const sel = document.getElementById('settingsTeamPresetModel');
|
const sel = document.getElementById('settingsTeamPresetModel');
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
sel.innerHTML = '<option value="">Select base model...</option>';
|
sel.innerHTML = '<option value="">Loading models...</option>';
|
||||||
const models = App.models.filter(m => !m.isPreset);
|
try {
|
||||||
models.forEach(m => {
|
const resp = await API.teamListAvailableModels(teamId);
|
||||||
const opt = document.createElement('option');
|
const models = resp.models || [];
|
||||||
opt.value = m.baseModelId || m.id;
|
sel.innerHTML = '<option value="">Select base model...</option>';
|
||||||
opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : '');
|
models.forEach(m => {
|
||||||
sel.appendChild(opt);
|
const opt = document.createElement('option');
|
||||||
});
|
opt.value = m.model_id || m.id;
|
||||||
|
const vis = m.visibility === 'team' ? ' [team-only]' : '';
|
||||||
|
opt.textContent = (m.model_id || m.id) + (m.provider_name ? ` (${m.provider_name})` : '') + vis;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to App.models if team endpoint fails
|
||||||
|
sel.innerHTML = '<option value="">Select base model...</option>';
|
||||||
|
App.models.filter(m => !m.isPreset).forEach(m => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = m.baseModelId || m.id;
|
||||||
|
opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : '');
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Providers ────────────────────────────
|
// ── Providers ────────────────────────────
|
||||||
@@ -1059,11 +1073,15 @@ const UI = {
|
|||||||
if (caps.tool_calling) badges.push('<span class="cap-badge cap-accent">🔧</span>');
|
if (caps.tool_calling) badges.push('<span class="cap-badge cap-accent">🔧</span>');
|
||||||
if (caps.vision) badges.push('<span class="cap-badge cap-accent">👁</span>');
|
if (caps.vision) badges.push('<span class="cap-badge cap-accent">👁</span>');
|
||||||
if (caps.thinking) badges.push('<span class="cap-badge cap-accent">💭</span>');
|
if (caps.thinking) badges.push('<span class="cap-badge cap-accent">💭</span>');
|
||||||
|
// Support both old is_enabled (bool) and new visibility (string)
|
||||||
|
const vis = m.visibility || (m.is_enabled === true ? 'enabled' : m.is_enabled === false ? 'disabled' : 'disabled');
|
||||||
|
const visLabel = vis === 'enabled' ? '✓ Enabled' : vis === 'team' ? '👥 Team' : 'Disabled';
|
||||||
|
const visClass = vis === 'enabled' ? 'enabled' : vis === 'team' ? 'team' : '';
|
||||||
return `<div class="admin-model-row">
|
return `<div class="admin-model-row">
|
||||||
<span class="model-name">${esc(m.model_id || m.id)}</span>
|
<span class="model-name">${esc(m.model_id || m.id)}</span>
|
||||||
<span class="model-caps-inline">${badges.join('')}</span>
|
<span class="model-caps-inline">${badges.join('')}</span>
|
||||||
<span class="provider-meta">${esc(m.provider_name || '')}</span>
|
<span class="provider-meta">${esc(m.provider_name || '')}</span>
|
||||||
<button class="admin-model-toggle ${m.is_enabled ? 'enabled' : ''}" onclick="toggleModel('${m.id}', ${!m.is_enabled})">${m.is_enabled ? '✓ Enabled' : 'Disabled'}</button>
|
<button class="admin-model-toggle ${visClass}" onclick="cycleModelVisibility('${m.id}', '${vis}')">${visLabel}</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('') || '<div class="empty-hint">No models — add a provider first, then click Fetch Models</div>';
|
}).join('') || '<div class="empty-hint">No models — add a provider first, then click Fetch Models</div>';
|
||||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||||
|
|||||||
Reference in New Issue
Block a user