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

@@ -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) {