Changeset 0.8.4 (#47)
This commit is contained in:
49
ROADMAP.md
49
ROADMAP.md
@@ -187,6 +187,55 @@ Required for enterprise and compliance. Cheap to build, expensive to retrofit.
|
|||||||
- [x] Admin audit viewer (filter by action, resource type, paginated)
|
- [x] Admin audit viewer (filter by action, resource type, paginated)
|
||||||
- [ ] Team admin sees audit entries scoped to their team
|
- [ ] Team admin sees audit entries scoped to their team
|
||||||
|
|
||||||
|
**Model Visibility (0.8.3)**
|
||||||
|
- [x] Three-state visibility: enabled (all users) / team (preset-building only) / disabled
|
||||||
|
- [x] Migration replaces `is_enabled` boolean with `visibility` varchar
|
||||||
|
- [x] Admin model list: 3-state cycle button, bulk set all to any state
|
||||||
|
- [x] `GET /teams/:teamId/models` returns enabled+team models for preset builders
|
||||||
|
- [x] User-facing `ListEnabledModels` strictly filters `visibility = 'enabled'`
|
||||||
|
|
||||||
|
**Preset Form Unification + UX (0.8.4)**
|
||||||
|
- [x] Single `renderPresetForm(options)` function shared across admin/team contexts
|
||||||
|
— parameterized: showAvatar, showProviderConfig, onSubmit, onCancel
|
||||||
|
- [x] Remove icon field from preset forms (not rendered anywhere meaningful)
|
||||||
|
- [x] Team admin preset form includes avatar upload (reuse admin component)
|
||||||
|
- [x] Hide Providers tab in Settings when `user_providers_enabled` is false
|
||||||
|
- [x] Settings Models tab: only show `visibility='enabled'` base models
|
||||||
|
(team-only models must not leak to user model list; presets filtered out)
|
||||||
|
- [x] Model provenance labels: badge indicating global vs personal source
|
||||||
|
— same model_id from both sources shown separately (different API keys)
|
||||||
|
- [x] Admin presets list: show created_by name + team name for attribution
|
||||||
|
(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
|
||||||
|
— lightweight filter, not access control
|
||||||
|
- [ ] User personal provider models: simple enable/disable toggle
|
||||||
|
(no team dimension — personal keys are personal)
|
||||||
|
|
||||||
|
**Team Providers (0.8.6+)**
|
||||||
|
- [ ] `team_id` column on `api_configs` (nullable, symmetric with `user_id`)
|
||||||
|
- [ ] Team admins manage team-scoped provider configs
|
||||||
|
(add/remove API keys, toggle active, same UI as personal providers)
|
||||||
|
- [ ] `allow_team_providers` flag in `teams.settings` JSONB
|
||||||
|
— admin controls which teams can add their own API keys
|
||||||
|
- [ ] Team provider models use same 3-state visibility: enabled/disabled/team
|
||||||
|
— enabled: team members see in model selector (direct use)
|
||||||
|
— team: team admin can use for presets only (not direct member access)
|
||||||
|
— disabled: hidden from everyone
|
||||||
|
Same cycle button, same bulk actions, scoped to team admin panel
|
||||||
|
- [ ] `ListEnabledModels` adds section: team provider models where
|
||||||
|
`visibility = 'enabled'` AND user is member of owning team
|
||||||
|
- [ ] Team preset builder shows team provider models where
|
||||||
|
`visibility IN ('enabled', 'team')` AND user is admin of owning team
|
||||||
|
- [ ] Three-tier provider hierarchy with uniform visibility semantics:
|
||||||
|
global (sys-admin) → team (team-admin) → personal (user)
|
||||||
|
- [ ] Model provenance badges: global / team:TeamName / personal
|
||||||
|
|
||||||
**Usage / Cost Tracking**
|
**Usage / Cost Tracking**
|
||||||
- [ ] Capture from provider responses: prompt_tokens, completion_tokens,
|
- [ ] Capture from provider responses: prompt_tokens, completion_tokens,
|
||||||
cache_creation_tokens, cache_read_tokens
|
cache_creation_tokens, cache_read_tokens
|
||||||
|
|||||||
@@ -434,6 +434,7 @@ func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
|
|||||||
ConfigID string `json:"config_id"`
|
ConfigID string `json:"config_id"`
|
||||||
Capabilities providers.ModelCapabilities `json:"capabilities"`
|
Capabilities providers.ModelCapabilities `json:"capabilities"`
|
||||||
Pricing *providers.ModelPricing `json:"pricing,omitempty"`
|
Pricing *providers.ModelPricing `json:"pricing,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
IsPreset bool `json:"is_preset,omitempty"`
|
IsPreset bool `json:"is_preset,omitempty"`
|
||||||
PresetID string `json:"preset_id,omitempty"`
|
PresetID string `json:"preset_id,omitempty"`
|
||||||
PresetScope string `json:"preset_scope,omitempty"`
|
PresetScope string `json:"preset_scope,omitempty"`
|
||||||
@@ -477,6 +478,7 @@ func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.Capabilities.MaxOutputTokens = providers.ResolveMaxOutput(m.ModelID, m.Capabilities)
|
m.Capabilities.MaxOutputTokens = providers.ResolveMaxOutput(m.ModelID, m.Capabilities)
|
||||||
|
m.Source = "global"
|
||||||
models = append(models, m)
|
models = append(models, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,6 +536,7 @@ func (h *APIConfigHandler) ListEnabledModels(c *gin.Context) {
|
|||||||
ConfigID: cfgID,
|
ConfigID: cfgID,
|
||||||
Capabilities: caps,
|
Capabilities: caps,
|
||||||
Pricing: pm.Pricing,
|
Pricing: pm.Pricing,
|
||||||
|
Source: "personal",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,10 +286,12 @@ func (h *PresetHandler) ListAdminPresets(c *gin.Context) {
|
|||||||
mp.scope, mp.team_id, mp.created_by, mp.is_shared, mp.is_active,
|
mp.scope, mp.team_id, mp.created_by, mp.is_shared, mp.is_active,
|
||||||
mp.icon, mp.avatar, mp.created_at, mp.updated_at,
|
mp.icon, mp.avatar, mp.created_at, mp.updated_at,
|
||||||
COALESCE(ac.name, '') as provider_name,
|
COALESCE(ac.name, '') as provider_name,
|
||||||
COALESCE(u.username, '') as creator_name
|
COALESCE(u.username, '') as creator_name,
|
||||||
|
COALESCE(t.name, '') as team_name
|
||||||
FROM model_presets mp
|
FROM model_presets mp
|
||||||
LEFT JOIN api_configs ac ON mp.api_config_id = ac.id
|
LEFT JOIN api_configs ac ON mp.api_config_id = ac.id
|
||||||
LEFT JOIN users u ON mp.created_by = u.id
|
LEFT JOIN users u ON mp.created_by = u.id
|
||||||
|
LEFT JOIN teams t ON mp.team_id = t.id
|
||||||
ORDER BY mp.scope ASC, mp.name ASC
|
ORDER BY mp.scope ASC, mp.name ASC
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -301,6 +303,7 @@ func (h *PresetHandler) ListAdminPresets(c *gin.Context) {
|
|||||||
type adminPreset struct {
|
type adminPreset struct {
|
||||||
presetResponse
|
presetResponse
|
||||||
CreatorName string `json:"creator_name"`
|
CreatorName string `json:"creator_name"`
|
||||||
|
TeamName string `json:"team_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
presets := make([]adminPreset, 0)
|
presets := make([]adminPreset, 0)
|
||||||
@@ -310,7 +313,8 @@ func (h *PresetHandler) ListAdminPresets(c *gin.Context) {
|
|||||||
&p.ID, &p.Name, &p.Description, &p.BaseModelID, &p.APIConfigID,
|
&p.ID, &p.Name, &p.Description, &p.BaseModelID, &p.APIConfigID,
|
||||||
&p.SystemPrompt, &p.Temperature, &p.MaxTokens, &p.ToolsEnabled,
|
&p.SystemPrompt, &p.Temperature, &p.MaxTokens, &p.ToolsEnabled,
|
||||||
&p.Scope, &p.TeamID, &p.CreatedBy, &p.IsShared, &p.IsActive,
|
&p.Scope, &p.TeamID, &p.CreatedBy, &p.IsShared, &p.IsActive,
|
||||||
&p.Icon, &p.Avatar, &p.CreatedAt, &p.UpdatedAt, &p.ProviderName, &p.CreatorName,
|
&p.Icon, &p.Avatar, &p.CreatedAt, &p.UpdatedAt, &p.ProviderName,
|
||||||
|
&p.CreatorName, &p.TeamName,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,7 @@
|
|||||||
<div class="modal-tabs settings-tabs">
|
<div class="modal-tabs settings-tabs">
|
||||||
<button class="settings-tab active" data-stab="general" onclick="UI.switchSettingsTab('general')">General</button>
|
<button class="settings-tab active" data-stab="general" onclick="UI.switchSettingsTab('general')">General</button>
|
||||||
<button class="settings-tab" data-stab="appearance" onclick="UI.switchSettingsTab('appearance')">Appearance</button>
|
<button class="settings-tab" data-stab="appearance" onclick="UI.switchSettingsTab('appearance')">Appearance</button>
|
||||||
<button class="settings-tab" data-stab="providers" onclick="UI.switchSettingsTab('providers')">Providers</button>
|
<button class="settings-tab" data-stab="providers" onclick="UI.switchSettingsTab('providers')" id="settingsProvidersTabBtn">Providers</button>
|
||||||
<button class="settings-tab" data-stab="models" onclick="UI.switchSettingsTab('models')">Models</button>
|
<button class="settings-tab" data-stab="models" onclick="UI.switchSettingsTab('models')">Models</button>
|
||||||
<button class="settings-tab" data-stab="teams" onclick="UI.switchSettingsTab('teams')" id="settingsTeamsTabBtn" style="display:none">Teams</button>
|
<button class="settings-tab" data-stab="teams" onclick="UI.switchSettingsTab('teams')" id="settingsTeamsTabBtn" style="display:none">Teams</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -331,22 +331,7 @@
|
|||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h4 style="font-size:13px;margin-bottom:6px">Team Presets</h4>
|
<h4 style="font-size:13px;margin-bottom:6px">Team Presets</h4>
|
||||||
<div id="settingsTeamPresets"></div>
|
<div id="settingsTeamPresets"></div>
|
||||||
<div id="settingsTeamAddPreset" style="display:none;margin-top:8px" class="admin-inline-form">
|
<div id="settingsTeamAddPreset" style="display:none;margin-top:8px" class="admin-inline-form"></div>
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group"><label>Name</label><input type="text" id="settingsTeamPresetName" placeholder="Code Reviewer"></div>
|
|
||||||
<div class="form-group"><label>Icon</label><input type="text" id="settingsTeamPresetIcon" placeholder="🔍" maxlength="4" style="width:60px"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group"><label>Description</label><input type="text" id="settingsTeamPresetDesc" placeholder="Reviews code for best practices"></div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group"><label>Base Model</label><select id="settingsTeamPresetModel"></select></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group"><label>System Prompt</label><textarea id="settingsTeamPresetPrompt" rows="3" placeholder="You are a code reviewer..."></textarea></div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group"><label>Temperature</label><input type="number" id="settingsTeamPresetTemp" step="0.1" min="0" max="2" placeholder="default"></div>
|
|
||||||
<div class="form-group"><label>Max Tokens</label><input type="number" id="settingsTeamPresetMaxTokens" placeholder="default"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row"><button class="btn-small btn-primary" id="settingsTeamCreatePresetBtn">Create</button><button class="btn-small" id="settingsTeamCancelPreset">Cancel</button></div>
|
|
||||||
</div>
|
|
||||||
<button class="btn-small" id="settingsTeamAddPresetBtn" style="margin-top:6px">+ New Team Preset</button>
|
<button class="btn-small" id="settingsTeamAddPresetBtn" style="margin-top:6px">+ New Team Preset</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -421,34 +406,7 @@
|
|||||||
<div class="admin-toolbar">
|
<div class="admin-toolbar">
|
||||||
<button class="btn-small btn-primary" id="adminAddPresetBtn">+ New Global Preset</button>
|
<button class="btn-small btn-primary" id="adminAddPresetBtn">+ New Global Preset</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="adminAddPresetForm" style="display:none" class="admin-inline-form">
|
<div id="adminAddPresetForm" style="display:none" class="admin-inline-form"></div>
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group"><label>Name</label><input type="text" id="adminPresetName" placeholder="Code Reviewer"></div>
|
|
||||||
<div class="form-group"><label>Icon</label><input type="text" id="adminPresetIcon" placeholder="🔍" maxlength="4" style="width:60px"></div>
|
|
||||||
</div>
|
|
||||||
<div class="avatar-upload-row" style="margin-bottom:0.75rem">
|
|
||||||
<div class="avatar-preview avatar-preview-sm" id="presetAvatarPreview">
|
|
||||||
<span id="presetAvatarPlaceholder">🤖</span>
|
|
||||||
</div>
|
|
||||||
<div class="avatar-actions">
|
|
||||||
<button class="btn-small" type="button" id="presetAvatarUploadBtn">Upload Avatar</button>
|
|
||||||
<button class="btn-small btn-danger" type="button" id="presetAvatarRemoveBtn" style="display:none">Remove</button>
|
|
||||||
<input type="file" id="presetAvatarFileInput" accept="image/png,image/jpeg,image/gif" style="display:none">
|
|
||||||
<div class="form-hint">Optional · 128×128 · shown in chat</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group"><label>Description</label><input type="text" id="adminPresetDesc" placeholder="Reviews code for best practices"></div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group"><label>Base Model</label><select id="adminPresetModel"></select></div>
|
|
||||||
<div class="form-group"><label>Provider Config</label><select id="adminPresetConfig"><option value="">Auto-resolve</option></select></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group"><label>System Prompt</label><textarea id="adminPresetPrompt" rows="3" placeholder="You are a code reviewer..."></textarea></div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group"><label>Temperature</label><input type="number" id="adminPresetTemp" step="0.1" min="0" max="2" placeholder="default"></div>
|
|
||||||
<div class="form-group"><label>Max Tokens</label><input type="number" id="adminPresetMaxTokens" placeholder="default"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row"><button class="btn-small btn-primary" id="adminCreatePresetBtn">Create</button><button class="btn-small" id="adminCancelPresetBtn">Cancel</button></div>
|
|
||||||
</div>
|
|
||||||
<div id="adminPresetList"></div>
|
<div id="adminPresetList"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-tab-content" id="adminTeamsTab" style="display:none">
|
<div class="admin-tab-content" id="adminTeamsTab" style="display:none">
|
||||||
|
|||||||
286
src/js/app.js
286
src/js/app.js
@@ -130,32 +130,6 @@ function updateAvatarPreview(dataURI) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preset avatar preview (admin form)
|
|
||||||
function updatePresetAvatarPreview(dataURI) {
|
|
||||||
const preview = document.getElementById('presetAvatarPreview');
|
|
||||||
const placeholder = document.getElementById('presetAvatarPlaceholder');
|
|
||||||
const removeBtn = document.getElementById('presetAvatarRemoveBtn');
|
|
||||||
if (!preview) return;
|
|
||||||
const existingImg = preview.querySelector('img');
|
|
||||||
|
|
||||||
if (dataURI) {
|
|
||||||
placeholder.style.display = 'none';
|
|
||||||
if (existingImg) {
|
|
||||||
existingImg.src = dataURI;
|
|
||||||
} else {
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = dataURI;
|
|
||||||
img.alt = 'Preset avatar';
|
|
||||||
preview.insertBefore(img, placeholder);
|
|
||||||
}
|
|
||||||
removeBtn.style.display = '';
|
|
||||||
} else {
|
|
||||||
if (existingImg) existingImg.remove();
|
|
||||||
placeholder.style.display = '';
|
|
||||||
removeBtn.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Models ───────────────────────────────────
|
// ── Models ───────────────────────────────────
|
||||||
|
|
||||||
// Client-side known model capabilities — used when backend hasn't synced yet.
|
// Client-side known model capabilities — used when backend hasn't synced yet.
|
||||||
@@ -997,72 +971,16 @@ function initListeners() {
|
|||||||
// Admin — models
|
// Admin — models
|
||||||
document.getElementById('adminFetchModelsBtn')?.addEventListener('click', fetchAdminModels);
|
document.getElementById('adminFetchModelsBtn')?.addEventListener('click', fetchAdminModels);
|
||||||
|
|
||||||
// Admin — presets
|
// Admin — presets (shared form)
|
||||||
document.getElementById('adminAddPresetBtn')?.addEventListener('click', () => {
|
document.getElementById('adminAddPresetBtn')?.addEventListener('click', () => {
|
||||||
|
const form = ensureAdminPresetForm();
|
||||||
|
if (!form) return;
|
||||||
_editingPresetId = null;
|
_editingPresetId = null;
|
||||||
document.getElementById('adminCreatePresetBtn').textContent = 'Create';
|
form.setSubmitLabel('Create');
|
||||||
document.getElementById('adminPresetName').value = '';
|
form.clearForm();
|
||||||
document.getElementById('adminPresetDesc').value = '';
|
|
||||||
document.getElementById('adminPresetPrompt').value = '';
|
|
||||||
document.getElementById('adminPresetIcon').value = '';
|
|
||||||
document.getElementById('adminPresetTemp').value = '';
|
|
||||||
document.getElementById('adminPresetMaxTokens').value = '';
|
|
||||||
updatePresetAvatarPreview(null);
|
|
||||||
const f = document.getElementById('adminAddPresetForm');
|
const f = document.getElementById('adminAddPresetForm');
|
||||||
f.style.display = f.style.display === 'none' ? '' : 'none';
|
f.style.display = f.style.display === 'none' ? '' : 'none';
|
||||||
});
|
});
|
||||||
document.getElementById('adminCancelPresetBtn')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('adminAddPresetForm').style.display = 'none';
|
|
||||||
_editingPresetId = null;
|
|
||||||
document.getElementById('adminCreatePresetBtn').textContent = 'Create';
|
|
||||||
updatePresetAvatarPreview(null);
|
|
||||||
});
|
|
||||||
document.getElementById('adminCreatePresetBtn')?.addEventListener('click', createAdminPreset);
|
|
||||||
|
|
||||||
// Admin — preset avatar
|
|
||||||
document.getElementById('presetAvatarUploadBtn')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('presetAvatarFileInput').click();
|
|
||||||
});
|
|
||||||
document.getElementById('presetAvatarFileInput')?.addEventListener('change', async function() {
|
|
||||||
const file = this.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
if (!_editingPresetId) {
|
|
||||||
// For new presets: show preview, upload after create
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => updatePresetAvatarPreview(reader.result);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
this.value = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// For existing presets: upload immediately
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async () => {
|
|
||||||
try {
|
|
||||||
const data = await API.adminUploadPresetAvatar(_editingPresetId, reader.result);
|
|
||||||
if (data.avatar) {
|
|
||||||
updatePresetAvatarPreview(data.avatar);
|
|
||||||
await UI.loadAdminPresets(true);
|
|
||||||
await fetchModels();
|
|
||||||
UI.toast('Preset avatar updated');
|
|
||||||
}
|
|
||||||
} catch (e) { UI.toast(e.message || 'Upload failed', 'error'); }
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
this.value = '';
|
|
||||||
});
|
|
||||||
document.getElementById('presetAvatarRemoveBtn')?.addEventListener('click', async () => {
|
|
||||||
if (!_editingPresetId) {
|
|
||||||
updatePresetAvatarPreview(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await API.adminDeletePresetAvatar(_editingPresetId);
|
|
||||||
updatePresetAvatarPreview(null);
|
|
||||||
await UI.loadAdminPresets(true);
|
|
||||||
await fetchModels();
|
|
||||||
UI.toast('Preset avatar removed');
|
|
||||||
} catch (e) { UI.toast(e.message || 'Remove failed', 'error'); }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin — Teams
|
// Admin — Teams
|
||||||
document.getElementById('adminAddTeamBtn')?.addEventListener('click', () => {
|
document.getElementById('adminAddTeamBtn')?.addEventListener('click', () => {
|
||||||
@@ -1138,37 +1056,40 @@ function initListeners() {
|
|||||||
document.getElementById('settingsTeamCancelMember')?.addEventListener('click', () => {
|
document.getElementById('settingsTeamCancelMember')?.addEventListener('click', () => {
|
||||||
document.getElementById('settingsTeamAddMember').style.display = 'none';
|
document.getElementById('settingsTeamAddMember').style.display = 'none';
|
||||||
});
|
});
|
||||||
|
var _teamPresetForm = null;
|
||||||
document.getElementById('settingsTeamAddPresetBtn')?.addEventListener('click', () => {
|
document.getElementById('settingsTeamAddPresetBtn')?.addEventListener('click', () => {
|
||||||
document.getElementById('settingsTeamAddPreset').style.display = '';
|
const container = document.getElementById('settingsTeamAddPreset');
|
||||||
UI.loadTeamPresetModelDropdown(UI._managingTeamId);
|
container.style.display = '';
|
||||||
});
|
if (!_teamPresetForm) {
|
||||||
document.getElementById('settingsTeamCancelPreset')?.addEventListener('click', () => {
|
_teamPresetForm = renderPresetForm(container, {
|
||||||
document.getElementById('settingsTeamAddPreset').style.display = 'none';
|
prefix: 'teamPreset',
|
||||||
});
|
showAvatar: true,
|
||||||
document.getElementById('settingsTeamCreatePresetBtn')?.addEventListener('click', async () => {
|
showProviderConfig: false,
|
||||||
|
onSubmit: async (vals) => {
|
||||||
const teamId = UI._managingTeamId;
|
const teamId = UI._managingTeamId;
|
||||||
const name = document.getElementById('settingsTeamPresetName').value.trim();
|
if (!vals.name) return UI.toast('Name required', 'error');
|
||||||
const baseModelId = document.getElementById('settingsTeamPresetModel').value;
|
if (!vals.base_model_id) return UI.toast('Select a base model', 'error');
|
||||||
if (!name) return UI.toast('Name required', 'error');
|
|
||||||
if (!baseModelId) return UI.toast('Select a base model', 'error');
|
|
||||||
const preset = {
|
|
||||||
name,
|
|
||||||
base_model_id: baseModelId,
|
|
||||||
description: document.getElementById('settingsTeamPresetDesc').value.trim(),
|
|
||||||
icon: document.getElementById('settingsTeamPresetIcon').value.trim(),
|
|
||||||
system_prompt: document.getElementById('settingsTeamPresetPrompt').value,
|
|
||||||
temperature: parseFloat(document.getElementById('settingsTeamPresetTemp').value) || undefined,
|
|
||||||
max_tokens: parseInt(document.getElementById('settingsTeamPresetMaxTokens').value) || undefined,
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
await API.teamCreatePreset(teamId, preset);
|
const result = await API.teamCreatePreset(teamId, vals);
|
||||||
document.getElementById('settingsTeamAddPreset').style.display = 'none';
|
// Upload pending avatar
|
||||||
['settingsTeamPresetName','settingsTeamPresetDesc','settingsTeamPresetIcon','settingsTeamPresetPrompt','settingsTeamPresetTemp','settingsTeamPresetMaxTokens']
|
const presetId = result?.id;
|
||||||
.forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
|
if (presetId && vals._pendingAvatar) {
|
||||||
|
try { await API.adminUploadPresetAvatar(presetId, vals._pendingAvatar); }
|
||||||
|
catch (e) { console.warn('Team preset avatar upload failed:', e.message); }
|
||||||
|
}
|
||||||
|
container.style.display = 'none';
|
||||||
|
_teamPresetForm.clearForm();
|
||||||
UI.toast('Team preset created');
|
UI.toast('Team preset created');
|
||||||
await UI.loadTeamManagePresets(teamId);
|
await UI.loadTeamManagePresets(teamId);
|
||||||
await fetchModels(); // refresh model selector
|
await fetchModels();
|
||||||
} catch (e) { UI.toast(e.message, 'error'); }
|
} catch (e) { UI.toast(e.message, 'error'); }
|
||||||
|
},
|
||||||
|
onCancel: () => { container.style.display = 'none'; _teamPresetForm.clearForm(); }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_teamPresetForm.clearForm();
|
||||||
|
}
|
||||||
|
UI.loadTeamPresetModelDropdown(UI._managingTeamId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin — banner controls
|
// Admin — banner controls
|
||||||
@@ -1650,50 +1571,103 @@ async function bulkSetVisibility(visibility) {
|
|||||||
|
|
||||||
// ── Admin Presets ────────────────────────────
|
// ── Admin Presets ────────────────────────────
|
||||||
|
|
||||||
|
var _adminPresetForm = null;
|
||||||
|
function ensureAdminPresetForm() {
|
||||||
|
if (_adminPresetForm) return _adminPresetForm;
|
||||||
|
const container = document.getElementById('adminAddPresetForm');
|
||||||
|
if (!container) return null;
|
||||||
|
_adminPresetForm = renderPresetForm(container, {
|
||||||
|
prefix: 'adminPreset',
|
||||||
|
showAvatar: true,
|
||||||
|
showProviderConfig: true,
|
||||||
|
onSubmit: (vals) => createAdminPreset(vals),
|
||||||
|
onCancel: () => {
|
||||||
|
container.style.display = 'none';
|
||||||
|
_editingPresetId = null;
|
||||||
|
_adminPresetForm.setSubmitLabel('Create');
|
||||||
|
_adminPresetForm.clearForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Override avatar handlers for edit-mode (upload immediately for existing presets)
|
||||||
|
const fileInput = document.getElementById('adminPreset_avatarFileInput');
|
||||||
|
if (fileInput) {
|
||||||
|
// Remove default handler set by renderPresetForm, add edit-aware one
|
||||||
|
const newInput = fileInput.cloneNode(true);
|
||||||
|
fileInput.parentNode.replaceChild(newInput, fileInput);
|
||||||
|
newInput.addEventListener('change', async function() {
|
||||||
|
const file = this.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async () => {
|
||||||
|
if (_editingPresetId) {
|
||||||
|
try {
|
||||||
|
const data = await API.adminUploadPresetAvatar(_editingPresetId, reader.result);
|
||||||
|
if (data.avatar) {
|
||||||
|
_adminPresetForm.updateAvatarPreview(data.avatar);
|
||||||
|
await UI.loadAdminPresets(true);
|
||||||
|
await fetchModels();
|
||||||
|
UI.toast('Preset avatar updated');
|
||||||
|
}
|
||||||
|
} catch (e) { UI.toast(e.message || 'Upload failed', 'error'); }
|
||||||
|
} else {
|
||||||
|
_adminPresetForm.updateAvatarPreview(reader.result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
this.value = '';
|
||||||
|
});
|
||||||
|
document.getElementById('adminPreset_avatarUploadBtn')?.addEventListener('click', () => newInput.click());
|
||||||
|
}
|
||||||
|
const removeBtn = document.getElementById('adminPreset_avatarRemoveBtn');
|
||||||
|
if (removeBtn) {
|
||||||
|
const newBtn = removeBtn.cloneNode(true);
|
||||||
|
removeBtn.parentNode.replaceChild(newBtn, removeBtn);
|
||||||
|
newBtn.addEventListener('click', async () => {
|
||||||
|
if (_editingPresetId) {
|
||||||
|
try {
|
||||||
|
await API.adminDeletePresetAvatar(_editingPresetId);
|
||||||
|
_adminPresetForm.updateAvatarPreview(null);
|
||||||
|
await UI.loadAdminPresets(true);
|
||||||
|
await fetchModels();
|
||||||
|
UI.toast('Preset avatar removed');
|
||||||
|
} catch (e) { UI.toast(e.message || 'Remove failed', 'error'); }
|
||||||
|
} else {
|
||||||
|
_adminPresetForm.updateAvatarPreview(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return _adminPresetForm;
|
||||||
|
}
|
||||||
|
|
||||||
var _editingPresetId = null;
|
var _editingPresetId = null;
|
||||||
function editAdminPreset(id) {
|
function editAdminPreset(id) {
|
||||||
const p = UI._presetCache?.[id];
|
const p = UI._presetCache?.[id];
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
_editingPresetId = id;
|
_editingPresetId = id;
|
||||||
const form = document.getElementById('adminAddPresetForm');
|
ensureAdminPresetForm();
|
||||||
form.style.display = '';
|
const form = _adminPresetForm;
|
||||||
document.getElementById('adminPresetName').value = p.name || '';
|
if (!form) return;
|
||||||
document.getElementById('adminPresetIcon').value = p.icon || '';
|
document.getElementById('adminAddPresetForm').style.display = '';
|
||||||
document.getElementById('adminPresetDesc').value = p.description || '';
|
form.setValues(p);
|
||||||
document.getElementById('adminPresetPrompt').value = p.system_prompt || '';
|
form.setSubmitLabel('Update');
|
||||||
document.getElementById('adminPresetTemp').value = p.temperature != null ? p.temperature : '';
|
|
||||||
document.getElementById('adminPresetMaxTokens').value = p.max_tokens != null ? p.max_tokens : '';
|
|
||||||
// Set base model and config dropdowns if present
|
|
||||||
const modelSel = document.getElementById('adminPresetModel');
|
|
||||||
if (modelSel) modelSel.value = p.base_model_id || '';
|
|
||||||
const cfgSel = document.getElementById('adminPresetConfig');
|
|
||||||
if (cfgSel) cfgSel.value = p.api_config_id || '';
|
|
||||||
// Avatar preview
|
|
||||||
updatePresetAvatarPreview(p.avatar || null);
|
|
||||||
document.getElementById('adminCreatePresetBtn').textContent = 'Update';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createAdminPreset() {
|
async function createAdminPreset(vals) {
|
||||||
const name = document.getElementById('adminPresetName').value.trim();
|
if (!vals) {
|
||||||
const baseModelId = document.getElementById('adminPresetModel').value;
|
if (_adminPresetForm) vals = _adminPresetForm.getValues();
|
||||||
if (!name || !baseModelId) { UI.toast('Name and base model are required', 'warning'); return; }
|
else return;
|
||||||
|
}
|
||||||
|
if (!vals.name || !vals.base_model_id) { UI.toast('Name and base model are required', 'warning'); return; }
|
||||||
|
|
||||||
const preset = {
|
const preset = {
|
||||||
name,
|
name: vals.name,
|
||||||
base_model_id: baseModelId,
|
base_model_id: vals.base_model_id,
|
||||||
description: document.getElementById('adminPresetDesc').value.trim(),
|
description: vals.description || '',
|
||||||
icon: document.getElementById('adminPresetIcon').value.trim(),
|
system_prompt: vals.system_prompt || '',
|
||||||
system_prompt: document.getElementById('adminPresetPrompt').value,
|
|
||||||
};
|
};
|
||||||
|
if (vals.api_config_id) preset.api_config_id = vals.api_config_id;
|
||||||
const configId = document.getElementById('adminPresetConfig').value;
|
if (vals.temperature != null) preset.temperature = vals.temperature;
|
||||||
if (configId) preset.api_config_id = configId;
|
if (vals.max_tokens != null) preset.max_tokens = vals.max_tokens;
|
||||||
|
|
||||||
const temp = parseFloat(document.getElementById('adminPresetTemp').value);
|
|
||||||
if (!isNaN(temp)) preset.temperature = temp;
|
|
||||||
|
|
||||||
const maxTok = parseInt(document.getElementById('adminPresetMaxTokens').value);
|
|
||||||
if (!isNaN(maxTok) && maxTok > 0) preset.max_tokens = maxTok;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let presetId = _editingPresetId;
|
let presetId = _editingPresetId;
|
||||||
@@ -1707,23 +1681,17 @@ async function createAdminPreset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upload pending avatar for new presets
|
// Upload pending avatar for new presets
|
||||||
const pendingImg = document.querySelector('#presetAvatarPreview img');
|
if (!_editingPresetId && presetId && vals._pendingAvatar) {
|
||||||
if (!_editingPresetId && presetId && pendingImg?.src?.startsWith('data:')) {
|
try { await API.adminUploadPresetAvatar(presetId, vals._pendingAvatar); }
|
||||||
try {
|
catch (e) { console.warn('Preset avatar upload failed:', e.message); }
|
||||||
await API.adminUploadPresetAvatar(presetId, pendingImg.src);
|
|
||||||
} catch (e) { console.warn('Preset avatar upload failed:', e.message); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_editingPresetId = null;
|
_editingPresetId = null;
|
||||||
document.getElementById('adminAddPresetForm').style.display = 'none';
|
document.getElementById('adminAddPresetForm').style.display = 'none';
|
||||||
document.getElementById('adminCreatePresetBtn').textContent = 'Create';
|
if (_adminPresetForm) {
|
||||||
document.getElementById('adminPresetName').value = '';
|
_adminPresetForm.setSubmitLabel('Create');
|
||||||
document.getElementById('adminPresetDesc').value = '';
|
_adminPresetForm.clearForm();
|
||||||
document.getElementById('adminPresetPrompt').value = '';
|
}
|
||||||
document.getElementById('adminPresetIcon').value = '';
|
|
||||||
document.getElementById('adminPresetTemp').value = '';
|
|
||||||
document.getElementById('adminPresetMaxTokens').value = '';
|
|
||||||
updatePresetAvatarPreview(null);
|
|
||||||
await UI.loadAdminPresets();
|
await UI.loadAdminPresets();
|
||||||
await fetchModels();
|
await fetchModels();
|
||||||
} catch (e) { UI.toast(e.message, 'error'); }
|
} catch (e) { UI.toast(e.message, 'error'); }
|
||||||
|
|||||||
176
src/js/ui.js
176
src/js/ui.js
@@ -22,6 +22,148 @@ function assistantAvatarURI(msg) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Shared Preset Form Component ────────────
|
||||||
|
// Renders a preset creation/edit form into a container element.
|
||||||
|
// Options: { prefix, showAvatar, showProviderConfig, onSubmit, onCancel }
|
||||||
|
// Returns: { getValues(), setValues(preset), clearForm(), container }
|
||||||
|
function renderPresetForm(containerEl, options = {}) {
|
||||||
|
const pfx = options.prefix || 'pf';
|
||||||
|
const showAvatar = options.showAvatar !== false;
|
||||||
|
const showConfig = !!options.showProviderConfig;
|
||||||
|
|
||||||
|
containerEl.innerHTML = `
|
||||||
|
<div class="form-group"><label>Name</label><input type="text" id="${pfx}_name" placeholder="Code Reviewer"></div>
|
||||||
|
${showAvatar ? `
|
||||||
|
<div class="avatar-upload-row" style="margin-bottom:0.75rem">
|
||||||
|
<div class="avatar-preview avatar-preview-sm" id="${pfx}_avatarPreview">
|
||||||
|
<span id="${pfx}_avatarPlaceholder">🤖</span>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-actions">
|
||||||
|
<button class="btn-small" type="button" id="${pfx}_avatarUploadBtn">Upload Avatar</button>
|
||||||
|
<button class="btn-small btn-danger" type="button" id="${pfx}_avatarRemoveBtn" style="display:none">Remove</button>
|
||||||
|
<input type="file" id="${pfx}_avatarFileInput" accept="image/png,image/jpeg,image/gif" style="display:none">
|
||||||
|
<div class="form-hint">Optional · 128×128 · shown in chat</div>
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
|
<div class="form-group"><label>Description</label><input type="text" id="${pfx}_desc" placeholder="Reviews code for best practices"></div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group"><label>Base Model</label><select id="${pfx}_model"></select></div>
|
||||||
|
${showConfig ? `<div class="form-group"><label>Provider Config</label><select id="${pfx}_config"><option value="">Auto-resolve</option></select></div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="form-group"><label>System Prompt</label><textarea id="${pfx}_prompt" rows="3" placeholder="You are a code reviewer..."></textarea></div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group"><label>Temperature</label><input type="number" id="${pfx}_temp" step="0.1" min="0" max="2" placeholder="default"></div>
|
||||||
|
<div class="form-group"><label>Max Tokens</label><input type="number" id="${pfx}_maxTokens" placeholder="default"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<button class="btn-small btn-primary" id="${pfx}_submitBtn">Create</button>
|
||||||
|
<button class="btn-small" id="${pfx}_cancelBtn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Avatar wiring
|
||||||
|
if (showAvatar) {
|
||||||
|
const uploadBtn = document.getElementById(`${pfx}_avatarUploadBtn`);
|
||||||
|
const fileInput = document.getElementById(`${pfx}_avatarFileInput`);
|
||||||
|
const removeBtn = document.getElementById(`${pfx}_avatarRemoveBtn`);
|
||||||
|
uploadBtn?.addEventListener('click', () => fileInput.click());
|
||||||
|
fileInput?.addEventListener('change', function() {
|
||||||
|
const file = this.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => form.updateAvatarPreview(reader.result);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
this.value = '';
|
||||||
|
});
|
||||||
|
removeBtn?.addEventListener('click', () => form.updateAvatarPreview(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel wiring
|
||||||
|
document.getElementById(`${pfx}_cancelBtn`)?.addEventListener('click', () => {
|
||||||
|
if (options.onCancel) options.onCancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit wiring
|
||||||
|
document.getElementById(`${pfx}_submitBtn`)?.addEventListener('click', () => {
|
||||||
|
if (options.onSubmit) options.onSubmit(form.getValues());
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = {
|
||||||
|
getValues() {
|
||||||
|
const v = {
|
||||||
|
name: document.getElementById(`${pfx}_name`)?.value.trim() || '',
|
||||||
|
description: document.getElementById(`${pfx}_desc`)?.value.trim() || '',
|
||||||
|
base_model_id: document.getElementById(`${pfx}_model`)?.value || '',
|
||||||
|
system_prompt: document.getElementById(`${pfx}_prompt`)?.value || '',
|
||||||
|
};
|
||||||
|
if (showConfig) {
|
||||||
|
const cfgId = document.getElementById(`${pfx}_config`)?.value;
|
||||||
|
if (cfgId) v.api_config_id = cfgId;
|
||||||
|
}
|
||||||
|
const temp = parseFloat(document.getElementById(`${pfx}_temp`)?.value);
|
||||||
|
if (!isNaN(temp)) v.temperature = temp;
|
||||||
|
const maxTok = parseInt(document.getElementById(`${pfx}_maxTokens`)?.value);
|
||||||
|
if (!isNaN(maxTok) && maxTok > 0) v.max_tokens = maxTok;
|
||||||
|
// Pending avatar data URI
|
||||||
|
const img = document.querySelector(`#${pfx}_avatarPreview img`);
|
||||||
|
if (img?.src?.startsWith('data:')) v._pendingAvatar = img.src;
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
setValues(p) {
|
||||||
|
const el = id => document.getElementById(`${pfx}_${id}`);
|
||||||
|
if (el('name')) el('name').value = p.name || '';
|
||||||
|
if (el('desc')) el('desc').value = p.description || '';
|
||||||
|
if (el('prompt')) el('prompt').value = p.system_prompt || '';
|
||||||
|
if (el('temp')) el('temp').value = p.temperature != null ? p.temperature : '';
|
||||||
|
if (el('maxTokens')) el('maxTokens').value = p.max_tokens != null ? p.max_tokens : '';
|
||||||
|
if (el('model')) el('model').value = p.base_model_id || '';
|
||||||
|
if (showConfig && el('config')) el('config').value = p.api_config_id || '';
|
||||||
|
if (showAvatar) form.updateAvatarPreview(p.avatar || null);
|
||||||
|
},
|
||||||
|
clearForm() {
|
||||||
|
['name','desc','prompt','temp','maxTokens'].forEach(f => {
|
||||||
|
const el = document.getElementById(`${pfx}_${f}`);
|
||||||
|
if (el) el.value = '';
|
||||||
|
});
|
||||||
|
const modelSel = document.getElementById(`${pfx}_model`);
|
||||||
|
if (modelSel) modelSel.selectedIndex = 0;
|
||||||
|
if (showConfig) {
|
||||||
|
const cfgSel = document.getElementById(`${pfx}_config`);
|
||||||
|
if (cfgSel) cfgSel.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
if (showAvatar) form.updateAvatarPreview(null);
|
||||||
|
},
|
||||||
|
updateAvatarPreview(dataURI) {
|
||||||
|
const preview = document.getElementById(`${pfx}_avatarPreview`);
|
||||||
|
const placeholder = document.getElementById(`${pfx}_avatarPlaceholder`);
|
||||||
|
const removeBtn = document.getElementById(`${pfx}_avatarRemoveBtn`);
|
||||||
|
if (!preview) return;
|
||||||
|
const existing = preview.querySelector('img');
|
||||||
|
if (dataURI) {
|
||||||
|
if (placeholder) placeholder.style.display = 'none';
|
||||||
|
if (existing) { existing.src = dataURI; }
|
||||||
|
else {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = dataURI; img.alt = 'Preset avatar';
|
||||||
|
preview.insertBefore(img, placeholder);
|
||||||
|
}
|
||||||
|
if (removeBtn) removeBtn.style.display = '';
|
||||||
|
} else {
|
||||||
|
if (existing) existing.remove();
|
||||||
|
if (placeholder) placeholder.style.display = '';
|
||||||
|
if (removeBtn) removeBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSubmitLabel(label) {
|
||||||
|
const btn = document.getElementById(`${pfx}_submitBtn`);
|
||||||
|
if (btn) btn.textContent = label;
|
||||||
|
},
|
||||||
|
getModelSelect() { return document.getElementById(`${pfx}_model`); },
|
||||||
|
getConfigSelect() { return showConfig ? document.getElementById(`${pfx}_config`) : null; },
|
||||||
|
};
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
const UI = {
|
const UI = {
|
||||||
|
|
||||||
// ── Sidebar ──────────────────────────────
|
// ── Sidebar ──────────────────────────────
|
||||||
@@ -623,6 +765,7 @@ const UI = {
|
|||||||
|
|
||||||
UI.loadProfileIntoSettings();
|
UI.loadProfileIntoSettings();
|
||||||
UI.loadMyTeams();
|
UI.loadMyTeams();
|
||||||
|
UI.checkUserProvidersAllowed();
|
||||||
UI.switchSettingsTab('general');
|
UI.switchSettingsTab('general');
|
||||||
openModal('settingsModal');
|
openModal('settingsModal');
|
||||||
},
|
},
|
||||||
@@ -693,10 +836,12 @@ const UI = {
|
|||||||
async checkUserProvidersAllowed() {
|
async checkUserProvidersAllowed() {
|
||||||
const notice = document.getElementById('userProvidersDisabled');
|
const notice = document.getElementById('userProvidersDisabled');
|
||||||
const addBtn = document.getElementById('providerShowAddBtn');
|
const addBtn = document.getElementById('providerShowAddBtn');
|
||||||
|
const tabBtn = document.getElementById('settingsProvidersTabBtn');
|
||||||
if (!notice) return;
|
if (!notice) return;
|
||||||
const allowed = App.serverSettings?.user_providers_enabled !== false;
|
const allowed = App.serverSettings?.user_providers_enabled !== false;
|
||||||
notice.style.display = allowed ? 'none' : '';
|
notice.style.display = allowed ? 'none' : '';
|
||||||
if (addBtn) addBtn.style.display = allowed ? '' : 'none';
|
if (addBtn) addBtn.style.display = allowed ? '' : 'none';
|
||||||
|
if (tabBtn) tabBtn.style.display = allowed ? '' : 'none';
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadProfileIntoSettings() {
|
async loadProfileIntoSettings() {
|
||||||
@@ -823,7 +968,7 @@ const UI = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async loadTeamPresetModelDropdown(teamId) {
|
async loadTeamPresetModelDropdown(teamId) {
|
||||||
const sel = document.getElementById('settingsTeamPresetModel');
|
const sel = document.getElementById('teamPreset_model');
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
sel.innerHTML = '<option value="">Loading models...</option>';
|
sel.innerHTML = '<option value="">Loading models...</option>';
|
||||||
try {
|
try {
|
||||||
@@ -1094,8 +1239,11 @@ const UI = {
|
|||||||
const data = await API.adminListPresets();
|
const data = await API.adminListPresets();
|
||||||
const list = data.presets || [];
|
const list = data.presets || [];
|
||||||
|
|
||||||
|
// Ensure form is initialized for dropdown population
|
||||||
|
ensureAdminPresetForm();
|
||||||
|
|
||||||
// Populate model dropdown from admin model list (all synced models)
|
// Populate model dropdown from admin model list (all synced models)
|
||||||
const modelSel = document.getElementById('adminPresetModel');
|
const modelSel = _adminPresetForm?.getModelSelect();
|
||||||
if (modelSel) {
|
if (modelSel) {
|
||||||
modelSel.innerHTML = '<option value="">Select base model...</option>';
|
modelSel.innerHTML = '<option value="">Select base model...</option>';
|
||||||
try {
|
try {
|
||||||
@@ -1113,7 +1261,7 @@ const UI = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Populate config dropdown
|
// Populate config dropdown
|
||||||
const cfgSel = document.getElementById('adminPresetConfig');
|
const cfgSel = _adminPresetForm?.getConfigSelect();
|
||||||
if (cfgSel && cfgSel.options.length <= 1) {
|
if (cfgSel && cfgSel.options.length <= 1) {
|
||||||
try {
|
try {
|
||||||
const cfgData = await API.adminListGlobalConfigs();
|
const cfgData = await API.adminListGlobalConfigs();
|
||||||
@@ -1132,12 +1280,21 @@ const UI = {
|
|||||||
UI._presetCache[p.id] = p;
|
UI._presetCache[p.id] = p;
|
||||||
const avatarEl = p.avatar
|
const avatarEl = p.avatar
|
||||||
? `<img src="${p.avatar}" class="preset-row-avatar" alt="">`
|
? `<img src="${p.avatar}" class="preset-row-avatar" alt="">`
|
||||||
: (p.icon ? `<span class="preset-row-icon">${p.icon}</span>` : '');
|
: '';
|
||||||
const scope = p.scope === 'global' ? '<span class="badge-admin">global</span>' : '<span class="badge-user">personal</span>';
|
// Scope badge with attribution
|
||||||
|
let scopeBadge = '';
|
||||||
|
if (p.scope === 'global') {
|
||||||
|
scopeBadge = '<span class="badge-admin">global</span>';
|
||||||
|
} else if (p.scope === 'team' && p.team_name) {
|
||||||
|
scopeBadge = `<span class="badge-team">👥 ${esc(p.team_name)}</span>`;
|
||||||
|
} else if (p.scope === 'personal') {
|
||||||
|
scopeBadge = '<span class="badge-user">personal</span>';
|
||||||
|
}
|
||||||
|
const creator = p.creator_name ? `<span class="text-muted" style="font-size:11px">by ${esc(p.creator_name)}</span>` : '';
|
||||||
const status = p.is_active ? '' : '<span class="badge-pending">inactive</span>';
|
const status = p.is_active ? '' : '<span class="badge-pending">inactive</span>';
|
||||||
return `<div class="admin-preset-row">
|
return `<div class="admin-preset-row">
|
||||||
<div class="preset-info">
|
<div class="preset-info">
|
||||||
<strong>${avatarEl}${esc(p.name)}</strong> ${scope} ${status}
|
<strong>${avatarEl}${esc(p.name)}</strong> ${scopeBadge} ${creator} ${status}
|
||||||
<div class="preset-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
|
<div class="preset-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
|
||||||
${p.description ? `<div class="preset-desc">${esc(p.description)}</div>` : ''}
|
${p.description ? `<div class="preset-desc">${esc(p.description)}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -1313,9 +1470,10 @@ const UI = {
|
|||||||
el.innerHTML = '<div class="loading">Loading models...</div>';
|
el.innerHTML = '<div class="loading">Loading models...</div>';
|
||||||
try {
|
try {
|
||||||
const data = await API.listEnabledModels();
|
const data = await API.listEnabledModels();
|
||||||
const models = data.models || [];
|
// Only show base models, not presets
|
||||||
|
const models = (data.models || []).filter(m => !m.is_preset);
|
||||||
if (!models.length) {
|
if (!models.length) {
|
||||||
el.innerHTML = '<div class="empty-hint">No models available — add a provider in the section above</div>';
|
el.innerHTML = '<div class="empty-hint">No models available</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.innerHTML = models.map(m => {
|
el.innerHTML = models.map(m => {
|
||||||
@@ -1325,10 +1483,12 @@ 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>');
|
||||||
|
const src = m.source === 'personal' ? '<span class="badge-user" style="font-size:10px">personal</span>' : '';
|
||||||
return `<div class="model-list-item">
|
return `<div class="model-list-item">
|
||||||
<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="model-provider">${esc(m.provider_name || m.provider || '')}</span>
|
<span class="model-provider">${esc(m.provider_name || m.provider || '')}</span>
|
||||||
|
${src}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
} 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