Changeset 0.10.1 (#57)

This commit is contained in:
2026-02-24 16:51:08 +00:00
parent ea03f956ca
commit 13772ebd6c
11 changed files with 472 additions and 159 deletions

View File

@@ -579,17 +579,38 @@ a:hover { text-decoration: underline; }
.side-panel {
width: 0; min-width: 0; overflow: hidden;
background: var(--bg); border-left: 1px solid var(--border);
display: flex; flex-direction: column;
display: flex; flex-direction: column; position: relative;
transition: width 0.25s ease, min-width 0.25s ease;
}
.side-panel.open {
width: 480px; min-width: 480px;
}
.side-panel.fullscreen {
position: fixed; top: 0; right: 0; bottom: 0;
width: 100% !important; min-width: 100% !important;
z-index: 100;
transition: none;
}
.side-panel-resize {
position: absolute; left: -3px; top: 0; bottom: 0; width: 6px;
cursor: col-resize; z-index: 10;
}
.side-panel-resize:hover { background: var(--accent); opacity: 0.3; }
.side-panel.fullscreen .side-panel-resize { display: none; }
.side-panel-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px; border-bottom: 1px solid var(--border);
background: var(--bg-raised); flex-shrink: 0;
}
.side-panel-actions {
display: flex; align-items: center; gap: 2px;
}
.side-panel-action-btn {
background: none; border: none; color: var(--text-3);
cursor: pointer; padding: 4px 6px; border-radius: var(--radius);
transition: all var(--transition); display: flex; align-items: center;
}
.side-panel-action-btn:hover { background: var(--bg-hover); color: var(--text); }
.side-panel-tabs {
display: flex; gap: 2px; background: var(--bg-surface);
border-radius: var(--radius); padding: 2px;
@@ -1279,6 +1300,12 @@ button { font-family: var(--font); cursor: pointer; }
.btn-approve { background: rgba(34,197,94,0.15); color: #4ade80; border: 1px solid rgba(34,197,94,0.3); border-radius: 4px; padding: 3px 10px; cursor: pointer; font-size: 12px; }
.team-card { padding: 8px 10px; background: rgba(255,255,255,0.03); border-radius: 6px; margin-bottom: 6px; }
.team-card-info { display: flex; align-items: center; gap: 8px; }
.team-admin-back {
cursor: pointer; margin-right: 4px; opacity: 0.5;
transition: opacity var(--transition);
}
.team-admin-back:hover { opacity: 1; }
.team-tab-content { padding: 0; }
.badge-private { background: rgba(139,92,246,0.85); color: #fff; font-size: 10px; padding: 1px 8px; border-radius: 4px; }
.admin-user-row.user-inactive { opacity: 0.7; }
.loading { color: var(--text-3); font-size: 13px; padding: 0.5rem; }

View File

@@ -95,6 +95,10 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
<span>Admin Panel</span>
</button>
<button class="flyout-item" id="menuTeamAdmin" style="display:none">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span>Team Management</span>
</button>
<button class="flyout-item" id="menuDebug">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 11V6a2 2 0 0 0-2-2h-1a2 2 0 0 0-2 2"/><path d="M9 6a2 2 0 0 0-2 2v3"/><path d="M4 14h4"/><path d="M16 14h4"/><path d="M12 18v-6"/><circle cx="12" cy="14" r="6"/></svg>
<span>Debug Log</span>
@@ -161,12 +165,21 @@
<!-- Side Panel (preview + notes) -->
<aside class="side-panel" id="sidePanel">
<div class="side-panel-resize" id="sidePanelResize"></div>
<div class="side-panel-header">
<div class="side-panel-tabs" id="sidePanelTabs">
<button class="side-panel-tab active" data-tab="preview" onclick="switchSidePanelTab('preview')">Preview</button>
<button class="side-panel-tab" data-tab="notes" onclick="switchSidePanelTab('notes')">Notes</button>
</div>
<button class="side-panel-close" onclick="closeSidePanel()" title="Close panel"></button>
<div class="side-panel-actions">
<button class="side-panel-action-btn" id="sidePanelClearBtn" onclick="clearPreview()" title="Clear preview" style="display:none">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
</button>
<button class="side-panel-action-btn" id="sidePanelFullscreenBtn" onclick="toggleSidePanelFullscreen()" title="Toggle fullscreen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
</button>
<button class="side-panel-close" onclick="closeSidePanel()" title="Close panel"></button>
</div>
</div>
<div class="side-panel-body">
<!-- Preview tab -->
@@ -321,8 +334,8 @@
<button class="settings-tab" data-stab="appearance" onclick="UI.switchSettingsTab('appearance')">Appearance</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="usage" onclick="UI.switchSettingsTab('usage')">Usage</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="personas" onclick="UI.switchSettingsTab('personas')" id="settingsPersonasTabBtn">Personas</button>
<button class="settings-tab" data-stab="usage" onclick="UI.switchSettingsTab('usage')" id="settingsUsageTabBtn">Usage</button>
</div>
<div class="modal-body">
<!-- General Tab -->
@@ -414,12 +427,15 @@
<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>
</div>
<!-- Personas Tab (hidden if admin disables user presets) -->
<div class="settings-tab-content" id="settingsPersonasTab" style="display:none">
<section class="settings-section">
<h3 style="font-size:14px;margin-bottom:4px">My Presets</h3>
<h3 style="font-size:14px;margin-bottom:4px">My Personas</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>
<button class="btn-small" id="userAddPresetBtn" style="margin-top:6px">+ New Persona</button>
</section>
</div>
<!-- Usage Tab -->
@@ -442,102 +458,114 @@
<div id="myUsageResults"></div>
</section>
</div>
<!-- Teams Tab (visible to team admins) -->
<div class="settings-tab-content" id="settingsTeamsTab" style="display:none">
<div id="settingsTeamPicker"></div>
<div id="settingsTeamManage" style="display:none">
<button class="notes-back-btn" id="settingsTeamBackBtn">← Back to teams</button>
<h3 id="settingsTeamManageName" style="margin:8px 0 4px;font-size:15px"></h3>
<!-- Members -->
<section class="settings-section">
<h4 style="font-size:13px;margin-bottom:6px">Members</h4>
<div id="settingsTeamMembers"></div>
<div id="settingsTeamAddMember" style="display:none;margin-top:8px" class="admin-inline-form">
<div class="form-row">
<div class="form-group"><label>User</label><select id="settingsTeamMemberUser"></select></div>
<div class="form-group"><label>Role</label><select id="settingsTeamMemberRole"><option value="member">Member</option><option value="admin">Team Admin</option></select></div>
</div>
<div class="form-row"><button class="btn-small btn-primary" id="settingsTeamAddMemberSubmit">Add</button><button class="btn-small" id="settingsTeamCancelMember">Cancel</button></div>
</div>
<button class="btn-small" id="settingsTeamAddMemberBtn" style="margin-top:6px">+ Add Member</button>
</section>
<!-- Team Providers -->
<section class="settings-section">
<h4 style="font-size:13px;margin-bottom:4px">Providers</h4>
<div id="settingsTeamProviders"></div>
<div id="settingsTeamEditProvider" style="display:none;margin-top:8px" class="admin-inline-form">
<div class="form-grid-2col">
<div class="form-group"><label>Name</label><input id="teamProviderEditName"></div>
<div class="form-group"><label>Endpoint</label><input id="teamProviderEditEndpoint"></div>
<div class="form-group"><label>API Key</label><input id="teamProviderEditKey" type="password" placeholder="(unchanged)"></div>
<div class="form-group"><label>Default Model</label><input id="teamProviderEditDefault" placeholder="optional"></div>
</div>
<div style="margin:6px 0"><label><input type="checkbox" id="teamProviderEditPrivate"> Private provider (data stays on-prem)</label></div>
<div class="form-row">
<button class="btn-small btn-primary" id="settingsTeamEditProviderSubmit">Update</button>
<button class="btn-small" id="settingsTeamCancelEditProvider">Cancel</button>
</div>
</div>
<div id="settingsTeamAddProvider" style="display:none;margin-top:8px" class="admin-inline-form">
<div class="form-grid-2col">
<div class="form-group"><label>Name</label><input id="teamProviderName" placeholder="e.g. Team OpenAI"></div>
<div class="form-group"><label>Provider</label><select id="teamProviderType"></select></div>
<div class="form-group"><label>Endpoint</label><input id="teamProviderEndpoint" placeholder="https://api.openai.com/v1"></div>
<div class="form-group"><label>API Key</label><input id="teamProviderKey" type="password" placeholder="sk-..."></div>
</div>
<div style="margin:6px 0"><label><input type="checkbox" id="teamProviderPrivate"> Private provider (data stays on-prem)</label></div>
<div class="form-row">
<button class="btn-small btn-primary" id="settingsTeamAddProviderSubmit">Add Provider</button>
<button class="btn-small" id="settingsTeamCancelProvider">Cancel</button>
</div>
</div>
<button class="btn-small" id="settingsTeamAddProviderBtn" style="margin-top:6px">+ Add Provider</button>
</section>
</div>
<div class="modal-footer"><button class="btn-primary" id="settingsSaveBtn">Save Settings</button></div>
</div>
</div>
<!-- Team Presets -->
<section class="settings-section">
<h4 style="font-size:13px;margin-bottom:6px">Team Presets</h4>
<div id="settingsTeamPresets"></div>
<div id="settingsTeamAddPreset" style="display:none;margin-top:8px" class="admin-inline-form"></div>
<button class="btn-small" id="settingsTeamAddPresetBtn" style="margin-top:6px">+ New Team Preset</button>
</section>
<!-- Team Usage (team admins only — usage against team-owned providers) -->
<section class="settings-section">
<h4 style="font-size:13px;margin-bottom:6px">Usage</h4>
<div class="form-row" style="gap:6px;margin-bottom:8px">
<select id="teamUsagePeriod" style="font-size:12px;padding:3px 6px" onchange="UI.loadTeamUsage()">
<option value="7d">7 days</option>
<option value="30d" selected>30 days</option>
<option value="90d">90 days</option>
</select>
<select id="teamUsageGroupBy" style="font-size:12px;padding:3px 6px" onchange="UI.loadTeamUsage()">
<option value="model">By Model</option>
<option value="user">By User</option>
<option value="day">By Day</option>
</select>
<!-- ── Team Management Modal ─────────────────── -->
<div class="modal-overlay" id="teamAdminModal">
<div class="modal modal-wide">
<div class="modal-header">
<h2 id="teamAdminTitle">Team Management</h2>
<button class="modal-close" id="teamAdminCloseBtn"></button>
</div>
<!-- Team picker (shown when user admins multiple teams) -->
<div id="teamAdminPicker" style="display:none">
<div class="modal-body" id="teamAdminPickerList"></div>
</div>
<!-- Tabbed management (shown after selecting a team) -->
<div id="teamAdminContent" style="display:none">
<div class="modal-tabs admin-tabs" id="teamAdminTabs">
<button class="admin-tab active" data-ttab="members" onclick="UI.switchTeamTab('members')">Members</button>
<button class="admin-tab" data-ttab="providers" onclick="UI.switchTeamTab('providers')">Providers</button>
<button class="admin-tab" data-ttab="presets" onclick="UI.switchTeamTab('presets')">Presets</button>
<button class="admin-tab" data-ttab="usage" onclick="UI.switchTeamTab('usage')">Usage</button>
<button class="admin-tab" data-ttab="activity" onclick="UI.switchTeamTab('activity')">Activity</button>
</div>
<div class="modal-body">
<!-- Members tab -->
<div class="team-tab-content" id="teamTabMembers">
<div id="settingsTeamMembers"></div>
<div id="settingsTeamAddMember" style="display:none;margin-top:8px" class="admin-inline-form">
<div class="form-row">
<div class="form-group"><label>User</label><select id="settingsTeamMemberUser"></select></div>
<div class="form-group"><label>Role</label><select id="settingsTeamMemberRole"><option value="member">Member</option><option value="admin">Team Admin</option></select></div>
</div>
<div id="settingsTeamUsageTotals"></div>
<div id="settingsTeamUsageResults"></div>
</section>
<!-- Team Activity (team admins only — audit scoped to team) -->
<section class="settings-section">
<h4 style="font-size:13px;margin-bottom:6px">Activity Log</h4>
<div class="audit-filters" style="margin-bottom:8px">
<select id="teamAuditFilterAction" style="font-size:12px;padding:3px 6px" onchange="UI.loadTeamAuditLog()">
<option value="">All actions</option>
</select>
<div class="form-row"><button class="btn-small btn-primary" id="settingsTeamAddMemberSubmit">Add</button><button class="btn-small" id="settingsTeamCancelMember">Cancel</button></div>
</div>
<button class="btn-small" id="settingsTeamAddMemberBtn" style="margin-top:6px">+ Add Member</button>
</div>
<!-- Providers tab -->
<div class="team-tab-content" id="teamTabProviders" style="display:none">
<div id="settingsTeamProviders"></div>
<div id="settingsTeamEditProvider" style="display:none;margin-top:8px" class="admin-inline-form">
<div class="form-grid-2col">
<div class="form-group"><label>Name</label><input id="teamProviderEditName"></div>
<div class="form-group"><label>Endpoint</label><input id="teamProviderEditEndpoint"></div>
<div class="form-group"><label>API Key</label><input id="teamProviderEditKey" type="password" placeholder="(unchanged)"></div>
<div class="form-group"><label>Default Model</label><input id="teamProviderEditDefault" placeholder="optional"></div>
</div>
<div id="settingsTeamAudit" style="max-height:300px;overflow-y:auto"></div>
<div id="teamAuditPagination" class="audit-pagination" style="display:none">
<button class="btn-small" id="teamAuditPrevBtn" onclick="UI.loadTeamAuditLog(UI._teamAuditPage - 1)">← Prev</button>
<span id="teamAuditPageInfo" style="font-size:11px;color:var(--text-3)"></span>
<button class="btn-small" id="teamAuditNextBtn" onclick="UI.loadTeamAuditLog(UI._teamAuditPage + 1)">Next →</button>
<div style="margin:6px 0"><label><input type="checkbox" id="teamProviderEditPrivate"> Private provider (data stays on-prem)</label></div>
<div class="form-row">
<button class="btn-small btn-primary" id="settingsTeamEditProviderSubmit">Update</button>
<button class="btn-small" id="settingsTeamCancelEditProvider">Cancel</button>
</div>
</section>
</div>
<div id="settingsTeamAddProvider" style="display:none;margin-top:8px" class="admin-inline-form">
<div class="form-grid-2col">
<div class="form-group"><label>Name</label><input id="teamProviderName" placeholder="e.g. Team OpenAI"></div>
<div class="form-group"><label>Provider</label><select id="teamProviderType"></select></div>
<div class="form-group"><label>Endpoint</label><input id="teamProviderEndpoint" placeholder="https://api.openai.com/v1"></div>
<div class="form-group"><label>API Key</label><input id="teamProviderKey" type="password" placeholder="sk-..."></div>
</div>
<div style="margin:6px 0"><label><input type="checkbox" id="teamProviderPrivate"> Private provider (data stays on-prem)</label></div>
<div class="form-row">
<button class="btn-small btn-primary" id="settingsTeamAddProviderSubmit">Add Provider</button>
<button class="btn-small" id="settingsTeamCancelProvider">Cancel</button>
</div>
</div>
<button class="btn-small" id="settingsTeamAddProviderBtn" style="margin-top:6px">+ Add Provider</button>
</div>
<!-- Presets tab -->
<div class="team-tab-content" id="teamTabPresets" style="display:none">
<div id="settingsTeamPresets"></div>
<div id="settingsTeamAddPreset" style="display:none;margin-top:8px" class="admin-inline-form"></div>
<button class="btn-small" id="settingsTeamAddPresetBtn" style="margin-top:6px">+ New Team Preset</button>
</div>
<!-- Usage tab -->
<div class="team-tab-content" id="teamTabUsage" style="display:none">
<div class="form-row" style="gap:6px;margin-bottom:8px">
<select id="teamUsagePeriod" style="font-size:12px;padding:3px 6px" onchange="UI.loadTeamUsage()">
<option value="7d">7 days</option>
<option value="30d" selected>30 days</option>
<option value="90d">90 days</option>
</select>
<select id="teamUsageGroupBy" style="font-size:12px;padding:3px 6px" onchange="UI.loadTeamUsage()">
<option value="model">By Model</option>
<option value="user">By User</option>
<option value="day">By Day</option>
</select>
</div>
<div id="settingsTeamUsageTotals"></div>
<div id="settingsTeamUsageResults"></div>
</div>
<!-- Activity tab -->
<div class="team-tab-content" id="teamTabActivity" style="display:none">
<div class="audit-filters" style="margin-bottom:8px">
<select id="teamAuditFilterAction" style="font-size:12px;padding:3px 6px" onchange="UI.loadTeamAuditLog()">
<option value="">All actions</option>
</select>
</div>
<div id="settingsTeamAudit" style="max-height:400px;overflow-y:auto"></div>
<div id="teamAuditPagination" class="audit-pagination" style="display:none">
<button class="btn-small" id="teamAuditPrevBtn" onclick="UI.loadTeamAuditLog(UI._teamAuditPage - 1)">← Prev</button>
<span id="teamAuditPageInfo" style="font-size:11px;color:var(--text-3)"></span>
<button class="btn-small" id="teamAuditNextBtn" onclick="UI.loadTeamAuditLog(UI._teamAuditPage + 1)">Next →</button>
</div>
</div>
</div>
</div>
<div class="modal-footer"><button class="btn-primary" id="settingsSaveBtn">Save Settings</button></div>
</div>
</div>
@@ -647,6 +675,13 @@
</div>
</div>
<div class="admin-tab-content" id="adminSettingsTab" style="display:none">
<section class="settings-section">
<h3>System Prompt</h3>
<p class="section-hint" style="margin-bottom:6px">Injected into every conversation before user/preset prompts. Users cannot override or disable this.</p>
<div class="form-group">
<textarea id="adminSystemPrompt" rows="4" placeholder="e.g. You are a helpful assistant for Acme Corp. Always respond professionally."></textarea>
</div>
</section>
<section class="settings-section">
<h3>Registration</h3>
<label class="checkbox-label"><input type="checkbox" id="adminRegToggle" checked> Allow new user registration</label>

View File

@@ -422,7 +422,7 @@ async function deleteChat(chatId) {
try {
await API.deleteChannel(chatId);
App.chats = App.chats.filter(c => c.id !== chatId);
if (App.currentChatId === chatId) newChat();
if (App.currentChatId === chatId) { clearPreview(); newChat(); }
UI.renderChatList();
} catch (e) { UI.toast('Failed to delete: ' + e.message, 'error'); }
}
@@ -907,6 +907,8 @@ function initListeners() {
// Flyout items
document.getElementById('menuSettings').addEventListener('click', () => { UI.closeUserMenu(); UI.openSettings(); });
document.getElementById('menuAdmin')?.addEventListener('click', () => { UI.closeUserMenu(); UI.openAdmin(); });
document.getElementById('menuTeamAdmin')?.addEventListener('click', () => { UI.closeUserMenu(); UI.openTeamAdmin(); });
document.getElementById('teamAdminCloseBtn')?.addEventListener('click', () => UI.closeTeamAdmin());
document.getElementById('menuDebug')?.addEventListener('click', () => { UI.closeUserMenu(); openDebugModal(); });
document.getElementById('menuSignout').addEventListener('click', () => { UI.closeUserMenu(); handleLogout(); });
@@ -1173,9 +1175,6 @@ function initListeners() {
});
// Settings — Team management (team admin self-service)
document.getElementById('settingsTeamBackBtn')?.addEventListener('click', () => {
UI.loadTeamsTab();
});
document.getElementById('settingsTeamAddMemberBtn')?.addEventListener('click', async () => {
document.getElementById('settingsTeamAddMember').style.display = '';
// Load available users (requires sys-admin; team admins use their own knowledge)
@@ -1698,6 +1697,10 @@ async function editProvider(id) {
async function handleSaveAdminSettings() {
try {
// Admin system prompt → global_settings (JSON value)
const sysPromptContent = document.getElementById('adminSystemPrompt').value.trim();
await API.adminUpdateSetting('system_prompt', { value: { content: sysPromptContent } });
// Policies — send as string "true"/"false" so backend routes to platform_policies
const reg = document.getElementById('adminRegToggle').checked;
await API.adminUpdateSetting('allow_registration', { value: reg ? 'true' : 'false' });

View File

@@ -827,6 +827,7 @@ const UI = {
UI.loadProfileIntoSettings();
UI.loadMyTeams();
UI.checkUserProvidersAllowed();
UI.checkUserPresetsAllowed();
UI.switchSettingsTab('general');
openModal('settingsModal');
},
@@ -842,10 +843,10 @@ const UI = {
UI.loadProviderList();
UI.checkUserProvidersAllowed();
}
if (tab === 'models') { UI.loadUserModels(); UI.loadUserPresets(); UI.checkUserPresetsAllowed(); }
if (tab === 'models') UI.loadUserModels();
if (tab === 'personas') { UI.loadUserPresets(); UI.checkUserPresetsAllowed(); }
if (tab === 'usage') UI.loadMyUsage();
if (tab === 'appearance') UI.loadAppearanceSettings();
if (tab === 'teams') UI.loadTeamsTab();
},
// ── Appearance Settings ─────────────────
@@ -899,19 +900,23 @@ const UI = {
const notice = document.getElementById('userProvidersDisabled');
const addBtn = document.getElementById('providerShowAddBtn');
const tabBtn = document.getElementById('settingsProvidersTabBtn');
const usageTabBtn = document.getElementById('settingsUsageTabBtn');
if (!notice) return;
const allowed = App.policies?.allow_user_byok === 'true';
notice.style.display = allowed ? 'none' : '';
if (addBtn) addBtn.style.display = allowed ? '' : 'none';
if (tabBtn) tabBtn.style.display = allowed ? '' : 'none';
if (usageTabBtn) usageTabBtn.style.display = allowed ? '' : 'none';
},
checkUserPresetsAllowed() {
const addBtn = document.getElementById('userAddPresetBtn');
const addForm = document.getElementById('userAddPresetForm');
const tabBtn = document.getElementById('settingsPersonasTabBtn');
const allowed = App.policies?.allow_user_personas === 'true';
if (addBtn) addBtn.style.display = allowed ? '' : 'none';
if (addForm && !allowed) addForm.style.display = 'none';
if (tabBtn) tabBtn.style.display = allowed ? '' : 'none';
},
async loadProfileIntoSettings() {
@@ -926,16 +931,16 @@ const UI = {
async loadMyTeams() {
const section = document.getElementById('settingsTeamsSection');
const el = document.getElementById('settingsTeamsList');
const tabBtn = document.getElementById('settingsTeamsTabBtn');
const menuBtn = document.getElementById('menuTeamAdmin');
if (!section || !el) return;
try {
const resp = await API.listMyTeams();
const teams = resp.data || [];
UI._myTeams = teams;
// Show/hide the Teams tab for team admins
// Show/hide the Team Management flyout item for team admins
const isTeamAdmin = teams.some(t => t.my_role === 'admin');
if (tabBtn) tabBtn.style.display = isTeamAdmin ? '' : 'none';
if (menuBtn) menuBtn.style.display = isTeamAdmin ? '' : 'none';
if (teams.length === 0) {
section.style.display = 'none';
@@ -943,67 +948,62 @@ const UI = {
}
section.style.display = '';
el.innerHTML = teams.map(t => `
<div class="team-card">
<div class="team-card"${t.my_role === 'admin' ? ` style="cursor:pointer" onclick="UI.openTeamManage('${t.id}', '${esc(t.name)}')"` : ''}>
<div class="team-card-info">
<strong>${esc(t.name)}</strong>
<span class="badge-${t.my_role === 'admin' ? 'admin' : 'user'}">${t.my_role}</span>
${t.my_role === 'admin' ? '<span style="margin-left:auto;color:var(--text-3);font-size:12px">Manage →</span>' : ''}
</div>
${t.description ? `<div class="text-muted" style="font-size:12px">${esc(t.description)}</div>` : ''}
<div class="text-muted" style="font-size:11px">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</div>
</div>
`).join('');
} catch (e) { section.style.display = 'none'; if (tabBtn) tabBtn.style.display = 'none'; }
} catch (e) { section.style.display = 'none'; if (menuBtn) menuBtn.style.display = 'none'; }
},
_myTeams: [],
_managingTeamId: null,
loadTeamsTab() {
const picker = document.getElementById('settingsTeamPicker');
const manage = document.getElementById('settingsTeamManage');
if (!picker) return;
manage.style.display = 'none';
picker.style.display = '';
const adminTeams = UI._myTeams.filter(t => t.my_role === 'admin');
if (adminTeams.length === 0) {
picker.innerHTML = '<div class="empty-hint">You are not an admin of any teams.</div>';
return;
}
picker.innerHTML = adminTeams.map(t => `
<div class="team-card" style="cursor:pointer" onclick="UI.openTeamManage('${t.id}', '${esc(t.name)}')">
<div class="team-card-info">
<strong>${esc(t.name)}</strong>
<span class="badge-admin">admin</span>
<span style="margin-left:auto;color:var(--text-3);font-size:12px">Manage →</span>
</div>
${t.description ? `<div class="text-muted" style="font-size:12px">${esc(t.description)}</div>` : ''}
<div class="text-muted" style="font-size:11px">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</div>
</div>
`).join('');
// No longer used — picker is in team admin modal
},
async openTeamManage(teamId, teamName) {
UI._managingTeamId = teamId;
UI._teamAuditPage = 1;
document.getElementById('settingsTeamPicker').style.display = 'none';
const manage = document.getElementById('settingsTeamManage');
manage.style.display = '';
document.getElementById('settingsTeamManageName').textContent = teamName;
// If the modal isn't open yet (called from settings "Manage →"), open it
if (!document.getElementById('teamAdminModal').classList.contains('active')) {
openModal('teamAdminModal');
}
// Show tabbed content, hide picker
const adminTeams = (UI._myTeams || []).filter(t => t.my_role === 'admin');
const titleEl = document.getElementById('teamAdminTitle');
if (adminTeams.length > 1) {
titleEl.innerHTML = `<span class="team-admin-back" onclick="UI.showTeamPicker()" title="Back to teams">←</span> ${esc(teamName)}`;
} else {
titleEl.textContent = teamName;
}
document.getElementById('teamAdminPicker').style.display = 'none';
document.getElementById('teamAdminContent').style.display = '';
// Reset forms
document.getElementById('settingsTeamAddMember').style.display = 'none';
document.getElementById('settingsTeamAddPreset').style.display = 'none';
document.getElementById('settingsTeamAddProvider').style.display = 'none';
document.getElementById('settingsTeamEditProvider').style.display = 'none';
// Reset team audit filter
const af = document.getElementById('teamAuditFilterAction');
if (af) { af.innerHTML = '<option value="">All actions</option>'; }
await Promise.all([
UI.loadTeamManageMembers(teamId),
UI.loadTeamManageProviders(teamId),
UI.loadTeamManagePresets(teamId),
UI.loadTeamUsage(),
UI.loadTeamAuditLog(1),
]);
// Show Members tab first, load its data
UI.switchTeamTab('members');
},
showTeamPicker() {
document.getElementById('teamAdminTitle').textContent = 'Team Management';
document.getElementById('teamAdminContent').style.display = 'none';
document.getElementById('teamAdminPicker').style.display = '';
},
async loadTeamManageMembers(teamId) {
@@ -1346,6 +1346,51 @@ const UI = {
openAdmin() { openModal('adminModal'); UI.switchAdminTab('users'); },
closeAdmin() { closeModal('adminModal'); },
openTeamAdmin() {
const adminTeams = (UI._myTeams || []).filter(t => t.my_role === 'admin');
if (adminTeams.length === 0) {
UI.toast('You are not an admin of any teams', 'error');
return;
}
openModal('teamAdminModal');
if (adminTeams.length === 1) {
// Skip picker, go directly to the team
UI.openTeamManage(adminTeams[0].id, adminTeams[0].name);
} else {
// Show team picker
document.getElementById('teamAdminTitle').textContent = 'Team Management';
document.getElementById('teamAdminPicker').style.display = '';
document.getElementById('teamAdminContent').style.display = 'none';
document.getElementById('teamAdminPickerList').innerHTML = adminTeams.map(t => `
<div class="team-card" style="cursor:pointer" onclick="UI.openTeamManage('${t.id}', '${esc(t.name)}')">
<div class="team-card-info">
<strong>${esc(t.name)}</strong>
<span class="badge-admin">admin</span>
<span style="margin-left:auto;color:var(--text-3);font-size:12px">Manage →</span>
</div>
${t.description ? `<div class="text-muted" style="font-size:12px">${esc(t.description)}</div>` : ''}
<div class="text-muted" style="font-size:11px">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</div>
</div>
`).join('');
}
},
closeTeamAdmin() { closeModal('teamAdminModal'); },
switchTeamTab(tab) {
document.querySelectorAll('#teamAdminTabs .admin-tab').forEach(t => t.classList.toggle('active', t.dataset.ttab === tab));
document.querySelectorAll('.team-tab-content').forEach(c => c.style.display = 'none');
const panel = document.getElementById(`teamTab${tab.charAt(0).toUpperCase() + tab.slice(1)}`);
if (panel) panel.style.display = '';
// Lazy-load tab data
const teamId = UI._managingTeamId;
if (tab === 'members') UI.loadTeamManageMembers(teamId);
if (tab === 'providers') UI.loadTeamManageProviders(teamId);
if (tab === 'presets') UI.loadTeamManagePresets(teamId);
if (tab === 'usage') UI.loadTeamUsage();
if (tab === 'activity') UI.loadTeamAuditLog(1);
},
async switchAdminTab(tab) {
document.querySelectorAll('.admin-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
document.querySelectorAll('.admin-tab-content').forEach(c => c.style.display = 'none');
@@ -1870,6 +1915,10 @@ const UI = {
// Registration (policy: allow_registration)
document.getElementById('adminRegToggle').checked = policies.allow_registration === 'true';
// Admin system prompt (global_settings)
const sysPrompt = getSetting('system_prompt', {}) || {};
document.getElementById('adminSystemPrompt').value = sysPrompt.content || '';
// Registration default state (policy: default_user_active → 'true' means auto-active)
const defaultActive = policies.default_user_active === 'true';
document.getElementById('adminRegDefaultState').value = defaultActive ? 'active' : 'pending';
@@ -2136,6 +2185,7 @@ function _formatMarked(text) {
let toolbar = collapseBtn;
toolbar += `<button class="copy-code-btn" onclick="navigator.clipboard.writeText(document.getElementById('${codeId}').textContent).then(()=>UI.toast('Copied','success'))">Copy</button>`;
toolbar += `<button class="copy-code-btn" onclick="downloadCode('${codeId}','${lang}')">Download</button>`;
// HTML preview button (only for HTML-like content)
if (lang === 'html' || lang === 'htm' || (!lang && _looksLikeHTML(code))) {
@@ -2164,6 +2214,7 @@ function _formatBasic(text) {
let toolbar = collapseBtn;
toolbar += `<button class="copy-code-btn" onclick="navigator.clipboard.writeText(document.getElementById('${id}').textContent).then(()=>UI.toast('Copied','success'))">Copy</button>`;
toolbar += `<button class="copy-code-btn" onclick="downloadCode('${id}','${lang}')">Download</button>`;
if (lang === 'html' || lang === 'htm' || (!lang && _looksLikeHTML(code))) {
toolbar += `<button class="copy-code-btn preview-code-btn" onclick="toggleHTMLPreview('${id}')">Preview</button>`;
@@ -2219,9 +2270,93 @@ function toggleHTMLPreview(codeId) {
frame.style.display = '';
if (empty) empty.style.display = 'none';
// Show clear button
const clearBtn = document.getElementById('sidePanelClearBtn');
if (clearBtn) clearBtn.style.display = '';
openSidePanel('preview');
}
function clearPreview() {
const frame = document.getElementById('previewFrame');
const empty = document.getElementById('previewEmpty');
const clearBtn = document.getElementById('sidePanelClearBtn');
if (frame) { frame.srcdoc = ''; frame.style.display = 'none'; }
if (empty) empty.style.display = '';
if (clearBtn) clearBtn.style.display = 'none';
}
function toggleSidePanelFullscreen() {
const panel = document.getElementById('sidePanel');
panel.classList.toggle('fullscreen');
const btn = document.getElementById('sidePanelFullscreenBtn');
if (btn) {
const isFS = panel.classList.contains('fullscreen');
btn.title = isFS ? 'Exit fullscreen' : 'Toggle fullscreen';
btn.innerHTML = isFS
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>'
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>';
}
}
function downloadCode(codeId, lang) {
const el = document.getElementById(codeId);
if (!el) return;
const extMap = {
javascript: 'js', typescript: 'ts', python: 'py', ruby: 'rb',
golang: 'go', go: 'go', rust: 'rs', java: 'java', cpp: 'cpp',
c: 'c', csharp: 'cs', css: 'css', html: 'html', htm: 'html',
json: 'json', yaml: 'yaml', yml: 'yml', xml: 'xml', sql: 'sql',
bash: 'sh', shell: 'sh', sh: 'sh', markdown: 'md', md: 'md',
toml: 'toml', dockerfile: 'Dockerfile', makefile: 'Makefile',
perl: 'pl', lua: 'lua', swift: 'swift', kotlin: 'kt', php: 'php',
};
const ext = extMap[lang?.toLowerCase()] || lang || 'txt';
const filename = `code.${ext}`;
const blob = new Blob([el.textContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename;
document.body.appendChild(a); a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
UI.toast('Downloaded ' + filename, 'success');
}
// ── Side Panel Resize ─────────────────────
(function initSidePanelResize() {
let startX, startW;
const panel = document.getElementById('sidePanel');
const handle = document.getElementById('sidePanelResize');
if (!handle || !panel) return;
handle.addEventListener('mousedown', (e) => {
if (panel.classList.contains('fullscreen')) return;
e.preventDefault();
startX = e.clientX;
startW = panel.getBoundingClientRect().width;
panel.style.transition = 'none'; // disable animation during drag
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
const onMove = (e) => {
const delta = startX - e.clientX; // dragging left = wider
const newW = Math.max(280, Math.min(window.innerWidth * 0.7, startW + delta));
panel.style.width = newW + 'px';
panel.style.minWidth = newW + 'px';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
panel.style.transition = ''; // re-enable animation
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
})();
function openSidePanel(tab) {
const panel = document.getElementById('sidePanel');
panel.classList.add('open');
@@ -2229,7 +2364,9 @@ function openSidePanel(tab) {
}
function closeSidePanel() {
document.getElementById('sidePanel').classList.remove('open');
const panel = document.getElementById('sidePanel');
panel.classList.remove('open', 'fullscreen');
panel.style.width = ''; panel.style.minWidth = '';
}
function switchSidePanelTab(tab) {