Changeset 0.8.5 (#48)
This commit is contained in:
15
ROADMAP.md
15
ROADMAP.md
@@ -208,14 +208,15 @@ Required for enterprise and compliance. Cheap to build, expensive to retrofit.
|
||||
(e.g. "Code Reviewer · 👥 Engineering · by sarah")
|
||||
|
||||
**User Presets + Model Filtering (0.8.5)**
|
||||
- [ ] Users can create personal presets from any enabled base model
|
||||
(backend `CreateUserPreset` exists, needs UI surface in Settings)
|
||||
- [ ] Settings Models tab reworked as "My Models": toggle to hide/show
|
||||
base models from selector, "+ Create Preset" action per model row
|
||||
- [ ] `user_model_preferences` table: user_id, model_config_id, hidden boolean
|
||||
- [x] Users can create personal presets from any enabled base model
|
||||
(removed user_providers_enabled gate; uses shared renderPresetForm)
|
||||
- [x] Settings Models tab reworked as "My Models": toggle to hide/show
|
||||
base models from selector, "My Presets" section with create/delete
|
||||
- [x] `user_model_preferences` table: user_id, model_id, hidden boolean
|
||||
— lightweight filter, not access control
|
||||
- [ ] User personal provider models: simple enable/disable toggle
|
||||
(no team dimension — personal keys are personal)
|
||||
- [x] Hidden models filtered from main model selector dropdown
|
||||
- [x] User personal provider models: simple enable/disable toggle
|
||||
(same visibility toggle — hide from selector)
|
||||
|
||||
**Team Providers (0.8.6+)**
|
||||
- [ ] `team_id` column on `api_configs` (nullable, symmetric with `user_id`)
|
||||
|
||||
@@ -141,6 +141,8 @@ check_column "api_configs" "is_global"
|
||||
check_table "user_model_preferences"
|
||||
check_column "user_model_preferences" "user_id"
|
||||
check_column "user_model_preferences" "model_config_id"
|
||||
check_column "user_model_preferences" "model_id"
|
||||
check_column "user_model_preferences" "hidden"
|
||||
|
||||
# ── 8. Channel unification (006-008) ────────
|
||||
echo ""
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- ==========================================
|
||||
-- Migration 019: (superseded by 020)
|
||||
-- ==========================================
|
||||
-- Original CREATE TABLE IF NOT EXISTS was a no-op because
|
||||
-- user_model_preferences already existed from migration 005.
|
||||
-- The actual rework is in 020_user_model_preferences_rework.sql.
|
||||
-- ==========================================
|
||||
SELECT 1;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- ==========================================
|
||||
-- Migration 019: User Model Preferences (rework)
|
||||
-- ==========================================
|
||||
-- The user_model_preferences table was created in 005 with:
|
||||
-- id UUID PK, user_id UUID, model_config_id UUID FK, is_enabled BOOL
|
||||
-- That schema ties preferences to model_configs rows (global only).
|
||||
-- We need string-based model_id to support personal provider models too,
|
||||
-- plus a 'hidden' column with clearer semantics.
|
||||
--
|
||||
-- Strategy: add new columns, add unique constraint for UPSERT.
|
||||
-- Old columns (model_config_id, is_enabled) remain for backward compat.
|
||||
-- ==========================================
|
||||
|
||||
-- Add new columns if they don't exist
|
||||
ALTER TABLE user_model_preferences ADD COLUMN IF NOT EXISTS model_id VARCHAR(255);
|
||||
ALTER TABLE user_model_preferences ADD COLUMN IF NOT EXISTS hidden BOOLEAN DEFAULT false;
|
||||
ALTER TABLE user_model_preferences ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
|
||||
|
||||
-- Make model_config_id nullable (new rows use model_id instead)
|
||||
ALTER TABLE user_model_preferences ALTER COLUMN model_config_id DROP NOT NULL;
|
||||
|
||||
-- Unique constraint for UPSERT — NULLs are distinct in PG so old rows won't conflict
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_model_pref_user_model
|
||||
ON user_model_preferences (user_id, model_id);
|
||||
125
server/handlers/model_prefs.go
Normal file
125
server/handlers/model_prefs.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetModelPreferences returns the user's hidden model list.
|
||||
// GET /api/v1/models/preferences
|
||||
func GetModelPreferences(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
rows, err := database.DB.Query(`
|
||||
SELECT model_id, hidden FROM user_model_preferences
|
||||
WHERE user_id = $1
|
||||
`, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to query preferences"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type pref struct {
|
||||
ModelID string `json:"model_id"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
prefs := make([]pref, 0)
|
||||
for rows.Next() {
|
||||
var p pref
|
||||
if err := rows.Scan(&p.ModelID, &p.Hidden); err != nil {
|
||||
continue
|
||||
}
|
||||
prefs = append(prefs, p)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"preferences": prefs})
|
||||
}
|
||||
|
||||
// SetModelPreference sets hidden state for a single model.
|
||||
// PUT /api/v1/models/preferences
|
||||
func SetModelPreference(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
var req struct {
|
||||
ModelID string `json:"model_id" binding:"required"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
_, err := database.DB.Exec(`
|
||||
INSERT INTO user_model_preferences (user_id, model_id, hidden, updated_at)
|
||||
VALUES ($1, $2, $3, NOW())
|
||||
ON CONFLICT (user_id, model_id)
|
||||
DO UPDATE SET hidden = EXCLUDED.hidden, updated_at = NOW()
|
||||
`, userID, req.ModelID, req.Hidden)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to save model preference for user %s, model %s: %v", userID, req.ModelID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save preference: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"model_id": req.ModelID, "hidden": req.Hidden})
|
||||
}
|
||||
|
||||
// BulkSetModelPreferences sets hidden state for multiple models at once.
|
||||
// POST /api/v1/models/preferences/bulk
|
||||
func BulkSetModelPreferences(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
var req struct {
|
||||
ModelIDs []string `json:"model_ids" binding:"required"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.ModelIDs) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"updated": 0})
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := database.DB.Begin()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to begin transaction"})
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
stmt, err := tx.Prepare(`
|
||||
INSERT INTO user_model_preferences (user_id, model_id, hidden, updated_at)
|
||||
VALUES ($1, $2, $3, NOW())
|
||||
ON CONFLICT (user_id, model_id)
|
||||
DO UPDATE SET hidden = EXCLUDED.hidden, updated_at = NOW()
|
||||
`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to prepare statement"})
|
||||
return
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
updated := 0
|
||||
for _, modelID := range req.ModelIDs {
|
||||
if _, err := stmt.Exec(userID, modelID, req.Hidden); err != nil {
|
||||
log.Printf("[WARN] Failed to save preference for model %s: %v", modelID, err)
|
||||
continue
|
||||
}
|
||||
updated++
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"updated": updated})
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -110,12 +111,6 @@ func (h *PresetHandler) ListUserPresets(c *gin.Context) {
|
||||
func (h *PresetHandler) CreateUserPreset(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
// Gate: personal presets require user_providers_enabled
|
||||
if !isUserProvidersEnabled() {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "personal presets are disabled by admin"})
|
||||
return
|
||||
}
|
||||
|
||||
var req createPresetRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -159,7 +154,8 @@ func (h *PresetHandler) CreateUserPreset(c *gin.Context) {
|
||||
).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create preset"})
|
||||
log.Printf("[WARN] Failed to create user preset: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create preset: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ func main() {
|
||||
protected.GET("/api-configs/:id/models", apiCfg.ListModels)
|
||||
protected.GET("/models", apiCfg.ListAllModels)
|
||||
protected.GET("/models/enabled", apiCfg.ListEnabledModels)
|
||||
protected.GET("/models/preferences", handlers.GetModelPreferences)
|
||||
protected.PUT("/models/preferences", handlers.SetModelPreference)
|
||||
protected.POST("/models/preferences/bulk", handlers.BulkSetModelPreferences)
|
||||
|
||||
// User Settings & Profile
|
||||
settings := handlers.NewSettingsHandler()
|
||||
|
||||
@@ -1013,6 +1013,8 @@ button { font-family: var(--font); cursor: pointer; }
|
||||
.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.team { border-color: #60a5fa; color: #60a5fa; }
|
||||
.model-list-item.model-hidden { opacity: 0.5; }
|
||||
.model-list-item.model-hidden .model-name { text-decoration: line-through; }
|
||||
|
||||
.admin-provider-row {
|
||||
display: flex; align-items: center; gap: 12px; padding: 10px 0;
|
||||
|
||||
@@ -305,8 +305,24 @@
|
||||
</div>
|
||||
<!-- Models Tab -->
|
||||
<div class="settings-tab-content" id="settingsModelsTab" style="display:none">
|
||||
<p class="section-hint">Models available from your providers and global configs.</p>
|
||||
<section class="settings-section">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||||
<h3 style="font-size:14px;margin:0">Available Models</h3>
|
||||
<div>
|
||||
<button class="btn-small" onclick="bulkSetUserModelVisibility(true)" title="Show all models in selector">Show All</button>
|
||||
<button class="btn-small" onclick="bulkSetUserModelVisibility(false)" title="Hide all models from selector">Hide All</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="section-hint" style="margin-bottom:8px">Toggle visibility in your model selector. Hidden models are still accessible through presets.</p>
|
||||
<div id="userModelList" class="model-list-grid"><div class="empty-hint">Loading models...</div></div>
|
||||
</section>
|
||||
<section class="settings-section">
|
||||
<h3 style="font-size:14px;margin-bottom:4px">My Presets</h3>
|
||||
<p class="section-hint" style="margin-bottom:8px">Personal model presets with custom system prompts and settings.</p>
|
||||
<div id="userPresetList"><div class="empty-hint">Loading...</div></div>
|
||||
<div id="userAddPresetForm" style="display:none;margin-top:8px" class="admin-inline-form"></div>
|
||||
<button class="btn-small" id="userAddPresetBtn" style="margin-top:6px">+ New Preset</button>
|
||||
</section>
|
||||
</div>
|
||||
<!-- Teams Tab (visible to team admins) -->
|
||||
<div class="settings-tab-content" id="settingsTeamsTab" style="display:none">
|
||||
|
||||
@@ -214,6 +214,15 @@ const API = {
|
||||
// ── Models ───────────────────────────────
|
||||
|
||||
listEnabledModels() { return this._get('/api/v1/models/enabled'); },
|
||||
getModelPreferences() { return this._get('/api/v1/models/preferences'); },
|
||||
setModelPreference(modelId, hidden) { return this._put('/api/v1/models/preferences', { model_id: modelId, hidden }); },
|
||||
bulkSetModelPreferences(modelIds, hidden) { return this._post('/api/v1/models/preferences/bulk', { model_ids: modelIds, hidden }); },
|
||||
|
||||
// User presets
|
||||
listUserPresets() { return this._get('/api/v1/presets'); },
|
||||
createUserPreset(preset) { return this._post('/api/v1/presets', preset); },
|
||||
updateUserPreset(id, updates) { return this._put(`/api/v1/presets/${id}`, updates); },
|
||||
deleteUserPreset(id) { return this._del(`/api/v1/presets/${id}`); },
|
||||
listAllModels() { return this._get('/api/v1/models'); },
|
||||
|
||||
// ── API Configs (user providers) ─────────
|
||||
|
||||
@@ -6,6 +6,7 @@ const App = {
|
||||
chats: [],
|
||||
currentChatId: null,
|
||||
models: [],
|
||||
hiddenModels: new Set(),
|
||||
isGenerating: false,
|
||||
abortController: null,
|
||||
serverSettings: {},
|
||||
@@ -200,10 +201,16 @@ function resolveCapabilities(backendCaps, modelId) {
|
||||
async function fetchModels() {
|
||||
try {
|
||||
const data = await API.listEnabledModels();
|
||||
// Load user model preferences
|
||||
try {
|
||||
const prefData = await API.getModelPreferences();
|
||||
App.hiddenModels = new Set(
|
||||
(prefData.preferences || []).filter(p => p.hidden).map(p => p.model_id)
|
||||
);
|
||||
} catch (e) { App.hiddenModels = new Set(); }
|
||||
|
||||
App.models = (data.models || []).map(m => {
|
||||
const isPreset = !!m.is_preset;
|
||||
// Presets: use preset_id as selector value, base model for caps
|
||||
// Regular: model_id is both
|
||||
const id = isPreset ? (m.preset_id || m.id) : (m.model_id || m.id);
|
||||
const baseModelId = m.model_id || m.id;
|
||||
return {
|
||||
@@ -218,6 +225,7 @@ async function fetchModels() {
|
||||
presetScope: m.preset_scope || null,
|
||||
presetAvatar: m.preset_avatar || null,
|
||||
presetTeamName: m.preset_team_name || null,
|
||||
hidden: !isPreset && App.hiddenModels.has(baseModelId),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -233,7 +241,7 @@ async function fetchModels() {
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
console.log(`📋 Loaded ${App.models.length} models (${App.models.filter(m => m.isPreset).length} presets)`);
|
||||
console.log(`📋 Loaded ${App.models.length} models (${App.models.filter(m => m.isPreset).length} presets, ${App.models.filter(m => m.hidden).length} hidden)`);
|
||||
} catch (e) { console.warn('Model fetch failed:', e.message); }
|
||||
UI.updateModelSelector();
|
||||
UI.updateCapabilityBadges();
|
||||
@@ -1092,6 +1100,49 @@ function initListeners() {
|
||||
UI.loadTeamPresetModelDropdown(UI._managingTeamId);
|
||||
});
|
||||
|
||||
// User — personal presets
|
||||
var _userPresetForm = null;
|
||||
document.getElementById('userAddPresetBtn')?.addEventListener('click', () => {
|
||||
const container = document.getElementById('userAddPresetForm');
|
||||
container.style.display = container.style.display === 'none' ? '' : 'none';
|
||||
if (!_userPresetForm) {
|
||||
_userPresetForm = renderPresetForm(container, {
|
||||
prefix: 'userPreset',
|
||||
showAvatar: true,
|
||||
showProviderConfig: false,
|
||||
onSubmit: async (vals) => {
|
||||
if (!vals.name) return UI.toast('Name required', 'error');
|
||||
if (!vals.base_model_id) return UI.toast('Select a base model', 'error');
|
||||
try {
|
||||
const result = await API.createUserPreset(vals);
|
||||
const presetId = result?.id;
|
||||
if (presetId && vals._pendingAvatar) {
|
||||
try { await API.adminUploadPresetAvatar(presetId, vals._pendingAvatar); }
|
||||
catch (e) { console.warn('User preset avatar upload failed:', e.message); }
|
||||
}
|
||||
container.style.display = 'none';
|
||||
_userPresetForm.clearForm();
|
||||
UI.toast('Preset created');
|
||||
await UI.loadUserPresets();
|
||||
await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
},
|
||||
onCancel: () => { container.style.display = 'none'; _userPresetForm.clearForm(); }
|
||||
});
|
||||
}
|
||||
// Populate model dropdown from user's available models
|
||||
const sel = _userPresetForm.getModelSelect();
|
||||
if (sel) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin — banner controls
|
||||
document.getElementById('adminBannerEnabled')?.addEventListener('change', (e) => {
|
||||
document.getElementById('bannerConfigFields').style.display = e.target.checked ? '' : 'none';
|
||||
@@ -1569,6 +1620,46 @@ async function bulkSetVisibility(visibility) {
|
||||
} catch (e) { UI.toast(e.message, 'error'); hint.textContent = 'Failed'; }
|
||||
}
|
||||
|
||||
// ── User Model Preferences ──────────────────
|
||||
|
||||
async function toggleUserModelVisibility(modelId, currentlyHidden) {
|
||||
try {
|
||||
await API.setModelPreference(modelId, !currentlyHidden);
|
||||
if (currentlyHidden) App.hiddenModels.delete(modelId);
|
||||
else App.hiddenModels.add(modelId);
|
||||
await UI.loadUserModels();
|
||||
await fetchModels(); // refresh model selector
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function bulkSetUserModelVisibility(show) {
|
||||
const models = App.models.filter(m => !m.isPreset);
|
||||
if (!models.length) return;
|
||||
const shouldHide = !show;
|
||||
// Only update models not already in the desired state
|
||||
const toUpdate = models
|
||||
.map(m => m.baseModelId || m.id)
|
||||
.filter(mid => App.hiddenModels.has(mid) !== shouldHide);
|
||||
if (!toUpdate.length) { UI.toast(show ? 'All already visible' : 'All already hidden'); return; }
|
||||
try {
|
||||
await API.bulkSetModelPreferences(toUpdate, shouldHide);
|
||||
toUpdate.forEach(mid => shouldHide ? App.hiddenModels.add(mid) : App.hiddenModels.delete(mid));
|
||||
await UI.loadUserModels();
|
||||
await fetchModels();
|
||||
UI.toast(show ? `${toUpdate.length} model(s) shown` : `${toUpdate.length} model(s) hidden`);
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function deleteUserPreset(id, name) {
|
||||
if (!confirm(`Delete preset "${name}"?`)) return;
|
||||
try {
|
||||
await API.deleteUserPreset(id);
|
||||
UI.toast('Preset deleted');
|
||||
await UI.loadUserPresets();
|
||||
await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
// ── Admin Presets ────────────────────────────
|
||||
|
||||
var _adminPresetForm = null;
|
||||
|
||||
52
src/js/ui.js
52
src/js/ui.js
@@ -533,7 +533,7 @@ const UI = {
|
||||
const globalPresets = App.models.filter(m => m.isPreset && m.presetScope === 'global');
|
||||
const teamPresets = App.models.filter(m => m.isPreset && m.presetScope === 'team');
|
||||
const personalPresets = App.models.filter(m => m.isPreset && m.presetScope === 'personal');
|
||||
const models = App.models.filter(m => !m.isPreset);
|
||||
const models = App.models.filter(m => !m.isPreset && !m.hidden);
|
||||
|
||||
const addGroup = (label, items) => {
|
||||
if (items.length === 0) return;
|
||||
@@ -573,7 +573,7 @@ const UI = {
|
||||
const settingsSel = document.getElementById('settingsModel');
|
||||
if (settingsSel) {
|
||||
settingsSel.innerHTML = '';
|
||||
App.models.forEach(m => {
|
||||
App.models.filter(m => !m.hidden).forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m.id;
|
||||
opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : '');
|
||||
@@ -781,7 +781,7 @@ const UI = {
|
||||
UI.loadProviderList();
|
||||
UI.checkUserProvidersAllowed();
|
||||
}
|
||||
if (tab === 'models') UI.loadUserModels();
|
||||
if (tab === 'models') { UI.loadUserModels(); UI.loadUserPresets(); }
|
||||
if (tab === 'appearance') UI.loadAppearanceSettings();
|
||||
if (tab === 'teams') UI.loadTeamsTab();
|
||||
},
|
||||
@@ -1469,14 +1469,24 @@ const UI = {
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="loading">Loading models...</div>';
|
||||
try {
|
||||
// Load preferences if not already loaded
|
||||
if (!App.hiddenModels) {
|
||||
try {
|
||||
const prefData = await API.getModelPreferences();
|
||||
App.hiddenModels = new Set(
|
||||
(prefData.preferences || []).filter(p => p.hidden).map(p => p.model_id)
|
||||
);
|
||||
} catch (e) { App.hiddenModels = new Set(); }
|
||||
}
|
||||
|
||||
const data = await API.listEnabledModels();
|
||||
// Only show base models, not presets
|
||||
const models = (data.models || []).filter(m => !m.is_preset);
|
||||
if (!models.length) {
|
||||
el.innerHTML = '<div class="empty-hint">No models available</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = models.map(m => {
|
||||
const mid = m.model_id || m.id;
|
||||
const caps = m.capabilities || {};
|
||||
const badges = [];
|
||||
if (caps.max_output_tokens > 0) badges.push(`<span class="cap-badge cap-context">${(caps.max_output_tokens/1000).toFixed(0)}K</span>`);
|
||||
@@ -1484,11 +1494,41 @@ const UI = {
|
||||
if (caps.vision) badges.push('<span class="cap-badge cap-accent">👁</span>');
|
||||
if (caps.thinking) badges.push('<span class="cap-badge cap-accent">💭</span>');
|
||||
const src = m.source === 'personal' ? '<span class="badge-user" style="font-size:10px">personal</span>' : '';
|
||||
return `<div class="model-list-item">
|
||||
<span class="model-name">${esc(m.model_id || m.id)}</span>
|
||||
const hidden = App.hiddenModels.has(mid);
|
||||
const toggleCls = hidden ? '' : 'enabled';
|
||||
const toggleLabel = hidden ? 'Hidden' : '✓ Visible';
|
||||
return `<div class="model-list-item ${hidden ? 'model-hidden' : ''}">
|
||||
<span class="model-name">${esc(mid)}</span>
|
||||
<span class="model-caps-inline">${badges.join('')}</span>
|
||||
<span class="model-provider">${esc(m.provider_name || m.provider || '')}</span>
|
||||
${src}
|
||||
<button class="admin-model-toggle ${toggleCls}" onclick="toggleUserModelVisibility('${esc(mid)}', ${hidden})" title="${hidden ? 'Show in selector' : 'Hide from selector'}">${toggleLabel}</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadUserPresets() {
|
||||
const el = document.getElementById('userPresetList');
|
||||
if (!el) return;
|
||||
try {
|
||||
const data = await API.listUserPresets();
|
||||
// Only show personal presets owned by user
|
||||
const presets = (data.presets || []).filter(p => p.scope === 'personal');
|
||||
if (!presets.length) {
|
||||
el.innerHTML = '<div class="empty-hint">No personal presets yet</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = presets.map(p => {
|
||||
const avatarEl = p.avatar ? `<img src="${p.avatar}" class="preset-row-avatar" alt="">` : '';
|
||||
return `<div class="admin-preset-row" style="padding:6px 0">
|
||||
<div class="preset-info">
|
||||
<strong>${avatarEl}${esc(p.name)}</strong>
|
||||
<div class="preset-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
|
||||
</div>
|
||||
<div class="preset-actions">
|
||||
<button class="btn-delete" onclick="deleteUserPreset('${p.id}', '${esc(p.name)}')" title="Delete">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
|
||||
Reference in New Issue
Block a user