Changeset 0.10.1 (#57)
This commit is contained in:
@@ -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' });
|
||||
|
||||
221
src/js/ui.js
221
src/js/ui.js
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user