This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/index.html
2026-03-01 12:40:15 +00:00

1240 lines
85 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
<base href="%%BASE_HREF%%">
<script>window.__BASE__ = '%%BASE_PATH%%';</script>
<script>window.__VERSION__ = '%%APP_VERSION%%'; if (window.__VERSION__.includes('%%')) window.__VERSION__ = 'dev';</script>
<script>window.__ENV__ = '%%ENVIRONMENT%%'; if (window.__ENV__.includes('%%')) window.__ENV__ = 'development';</script>
<script>try { window.__BRANDING__ = %%BRANDING_JSON%%; } catch(e) { window.__BRANDING__ = {}; }</script>
<title>Chat Switchboard</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg?v=%%APP_VERSION%%">
<link rel="icon" type="image/x-icon" href="favicon.ico?v=%%APP_VERSION%%">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png?v=%%APP_VERSION%%">
<link rel="icon" type="image/png" sizes="256x256" href="favicon-256.png?v=%%APP_VERSION%%">
<link rel="apple-touch-icon" sizes="256x256" href="favicon-256.png?v=%%APP_VERSION%%">
<link rel="manifest" href="manifest.json?v=%%APP_VERSION%%">
<meta name="theme-color" content="#0e0e10">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="stylesheet" href="css/styles.css?v=%%APP_VERSION%%">
<link rel="stylesheet" href="css/persona-kb.css?v=%%APP_VERSION%%">
<link rel="stylesheet" href="css/memory.css?v=%%APP_VERSION%%">
<link rel="stylesheet" href="css/notifications.css?v=%%APP_VERSION%%">
<link rel="stylesheet" href="css/channel-models.css?v=%%APP_VERSION%%">
<link rel="stylesheet" href="css/notification-prefs.css?v=%%APP_VERSION%%">
<link rel="stylesheet" href="branding/custom.css" onerror="this.remove()">
</head>
<body>
<!-- ── App Shell ───────────────────────────── -->
<div id="appContainer" class="app" style="display:none">
<!-- Environment Banner (top) -->
<div class="banner banner-top" id="bannerTop"></div>
<div class="app-body">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-top">
<button class="sb-brand" id="sidebarToggle" title="Toggle sidebar">
<span class="brand-logo" id="brandSidebarLogo"><img src="favicon-32.png" class="brand-logo-img" alt=""></span>
<svg class="brand-collapse" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
<span class="sb-label brand-text" id="brandSidebarText">Chat Switchboard</span>
</button>
<div class="split-btn" style="position:relative">
<button class="split-btn-main" id="newChatBtn" title="New Chat">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
<span class="sb-label">New Chat</span>
</button>
<button class="split-btn-drop" id="newChatDropBtn" title="More options">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="split-dropdown" id="newChatDropdown">
<button class="split-dropdown-item" onclick="newChat()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
New Chat
</button>
<button class="split-dropdown-item" onclick="createProject()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
New Project
</button>
<button class="split-dropdown-item disabled" title="Coming soon">
<svg width="14" height="14" 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>
Group Chat <span class="dd-hint">soon</span>
</button>
<button class="split-dropdown-item disabled" title="Coming soon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
New Channel <span class="dd-hint">soon</span>
</button>
</div>
</div>
</div>
<div class="sidebar-search" id="sidebarSearch">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="chatSearchInput" placeholder="Search chats…" autocomplete="off">
<button class="sidebar-search-clear" id="chatSearchClear" title="Clear"></button>
</div>
<div class="sidebar-chats" id="chatHistory"></div>
<!-- Notes shortcut -->
<div class="sidebar-notes-btn">
<button class="sb-notes" id="notesBtn" title="Notes">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<span class="sb-label">Notes</span>
</button>
</div>
<!-- User area -->
<div class="sidebar-bottom">
<button class="user-btn" id="userMenuBtn">
<div class="user-avatar" id="userAvatar"><span id="avatarLetter">?</span>
<span class="avatar-bug" title="Debug available">🐛</span>
</div>
<span class="sb-label user-name" id="userName">User</span>
</button>
<div class="user-flyout" id="userFlyout">
<button class="flyout-item" id="menuSettings">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
<span>Settings</span>
</button>
<button class="flyout-item" id="menuAdmin" style="display:none">
<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>
</button>
<div class="flyout-divider"></div>
<button class="flyout-item flyout-danger" id="menuSignout">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
<span>Sign Out</span>
</button>
</div>
</div>
</aside>
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- Main Chat Area -->
<main class="chat-area">
<div class="model-bar">
<button class="mobile-menu-btn" id="mobileMenuBtn" title="Menu">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="model-dropdown" id="modelDropdown">
<button class="model-dropdown-btn" id="modelDropdownBtn" title="Select model">
<span class="model-dropdown-label" id="modelDropdownLabel">Select a model</span>
<svg width="10" height="10" viewBox="0 0 10 10" fill="currentColor"><path d="M2 3.5L5 6.5L8 3.5"/></svg>
</button>
<div class="model-dropdown-menu" id="modelDropdownMenu"></div>
</div>
<button class="icon-btn" id="fetchModelsBtn" title="Refresh models">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
</button>
<div class="model-caps" id="modelCaps"></div>
<div class="ch-model-pills" id="channelModelPills"></div>
<span class="chat-token-count" id="chatTokenCount"></span>
<div class="notif-wrap" id="notifWrap">
<button class="notif-bell" id="notifBell" onclick="Notifications.toggleDropdown()" title="Notifications">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="notif-badge" id="notifBadge" style="display:none">0</span>
</button>
<div class="notif-dropdown" id="notifDropdown"></div>
</div>
</div>
<div class="messages" id="chatMessages">
<div class="empty-state">
<div class="empty-logo"><img src="favicon-256.png" class="empty-logo-img" alt=""></div>
<h2>Chat Switchboard</h2>
<p>Select a model and start chatting</p>
</div>
</div>
<div class="input-area">
<div class="context-warning" id="contextWarning" style="display:none">
<span class="context-warning-icon">⚠️</span>
<span class="context-warning-text" id="contextWarningText"></span>
<button class="context-warning-action" id="summarizeBtn" onclick="summarizeAndContinue()" title="Summarize conversation and continue with reduced context" style="display:none">📝 Summarize & Continue</button>
<label class="context-warning-toggle" id="autoCompactToggle" style="display:none" title="When enabled, this conversation will be automatically compacted when context gets long">
<input type="checkbox" id="autoCompactCheck" onchange="toggleChannelAutoCompact(this.checked)"> Auto
</label>
<button class="context-warning-dismiss" onclick="dismissContextWarning()" title="Dismiss"></button>
</div>
<div class="attachment-strip" id="attachmentStrip" style="display:none"></div>
<div class="input-wrap">
<div class="input-toolbar">
<button class="action-btn attach-btn" id="attachBtn" title="Attach file" style="display:none">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>
</button>
<input type="file" id="fileInput" multiple style="display:none">
<div class="tools-toggle-wrap" style="display:none">
<button class="action-btn tools-toggle-btn" id="toolsToggleBtn" title="Tools">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
</button>
<div class="popup-menu tools-popup" id="toolsPopup"></div>
</div>
<div class="kb-toggle-wrap">
<button class="action-btn kb-toggle-btn" id="kbToggleBtn" title="Knowledge Bases">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
</button>
<div class="popup-menu kb-popup" id="kbPopup"></div>
</div>
</div>
<div id="messageInputWrap">
<textarea id="messageInput" placeholder="Send a message..." rows="1"></textarea>
</div>
<div class="input-actions">
<button class="action-btn stop-btn" id="stopBtn" title="Stop">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
</button>
<button class="action-btn send-btn" id="sendBtn" title="Send">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</div>
<div class="input-meta" id="inputMeta">
<span class="input-token-count" id="inputTokenCount"></span>
</div>
</div>
</main>
<!-- Side Panel (registry-driven, see panels.js) -->
<aside class="side-panel" id="sidePanel">
<div class="side-panel-resize" id="sidePanelResize"></div>
<div class="side-panel-header">
<span class="side-panel-label" id="sidePanelLabel"></span>
<div class="side-panel-actions">
<span id="sidePanelPanelActions"></span>
<button class="side-panel-action-btn side-panel-dual-btn" id="sidePanelDualBtn" onclick="PanelRegistry.toggleDual()" title="Toggle split view" style="display:none">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></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="PanelRegistry.closeAll()" title="Close panel"></button>
</div>
</div>
<div class="side-panel-body" id="sidePanelBody">
<!-- Dual-view divider (hidden by default, shown in dual mode) -->
<div class="side-panel-divider" id="sidePanelDivider" style="display:none"></div>
<!-- Preview panel -->
<div class="side-panel-page" id="sidePanelPreview" style="display:none">
<div class="side-panel-empty" id="previewEmpty">
<p>Click <strong>Preview</strong> on any HTML code block to render it here.</p>
</div>
<iframe id="previewFrame" class="preview-frame" sandbox="allow-scripts" style="display:none"></iframe>
</div>
<!-- Notes panel -->
<div class="side-panel-page" id="sidePanelNotes" style="display:none">
<div class="notes-toolbar">
<div class="notes-search-wrap">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="notesSearchInput" placeholder="Search notes..." autocomplete="off">
</div>
<select id="notesFolderFilter" class="notes-filter-select">
<option value="">All folders</option>
</select>
<select id="notesSortSelect" class="notes-filter-select" title="Sort by">
<option value="updated_desc">Updated ↓</option>
<option value="updated_asc">Updated ↑</option>
<option value="created_desc">Created ↓</option>
<option value="created_asc">Created ↑</option>
<option value="title_asc">Title AZ</option>
<option value="title_desc">Title ZA</option>
</select>
</div>
<div class="notes-actions-bar">
<button class="btn-small" id="notesSelectModeBtn">Select</button>
<button class="btn-small" id="notesGraphBtn" title="Knowledge graph">Graph</button>
<button class="btn-small" id="notesTodayBtn" title="Today's daily note">Today</button>
<button class="btn-small btn-primary" id="notesNewBtn">+ New Note</button>
</div>
<div class="notes-selection-bar" id="notesSelectionBar" style="display:none">
<label class="notes-select-all"><input type="checkbox" id="notesSelectAll"> <span id="notesSelectedCount">0</span> selected</label>
<button class="btn-small btn-danger" id="notesDeleteSelectedBtn">Delete selected</button>
<button class="btn-small" id="notesCancelSelectBtn">Cancel</button>
</div>
<!-- List view (default) -->
<div id="notesListView">
<div id="notesList" class="notes-list">
<div class="notes-empty">No notes yet. Create one or ask the AI to save a note.</div>
</div>
</div>
<!-- Graph view (hidden by default) -->
<div id="notesGraphView" class="notes-graph-view" style="display:none">
<div class="notes-graph-toolbar">
<button class="btn-small" onclick="closeNoteGraph()">← List</button>
<span class="notes-graph-stats">
<span id="graphNodeCount">0</span> notes ·
<span id="graphEdgeCount">0</span> links
</span>
<button class="btn-small" onclick="_graphToggleOrphans()" title="Toggle unresolved links">Ghosts</button>
<button class="btn-small" onclick="_graphResetZoom()" title="Reset zoom">Reset</button>
</div>
<canvas id="noteGraphCanvas"></canvas>
</div>
<!-- Editor view (hidden by default) -->
<div id="notesEditorView" style="display:none">
<div class="notes-editor">
<div class="notes-editor-toolbar">
<button class="notes-back-btn" id="notesBackBtn">← Back to list</button>
<div class="notes-mode-toggle">
<button class="btn-small" id="noteEditBtn" style="display:none">Edit</button>
<button class="btn-small" id="notePreviewBtn" style="display:none">Preview</button>
</div>
</div>
<!-- Read mode: rendered markdown -->
<div id="noteReadMode" style="display:none">
<h3 class="note-read-title" id="noteReadTitle"></h3>
<div class="note-read-meta" id="noteReadMeta"></div>
<div class="note-read-content msg-text" id="noteReadContent"></div>
<!-- Backlinks panel -->
<div id="noteBacklinks" class="note-backlinks" style="display:none">
<div class="note-backlinks-header" onclick="toggleBacklinks()">
<span>Linked mentions</span>
<span id="noteBacklinksCount" class="badge">0</span>
</div>
<div id="noteBacklinksList" class="note-backlinks-list"></div>
</div>
<div class="notes-editor-actions">
<button class="btn-small" id="noteCopyBtn" onclick="copyNoteContent()">Copy</button>
<button class="btn-small btn-danger" id="noteDeleteBtn2" style="display:none">Delete</button>
</div>
</div>
<!-- Edit mode: form fields -->
<div id="noteEditMode">
<div class="form-group"><input type="text" id="noteEditorTitle" placeholder="Note title" class="notes-title-input"></div>
<div class="form-row">
<div class="form-group"><input type="text" id="noteEditorFolder" placeholder="Folder (e.g. /work/meetings)" class="notes-folder-input"></div>
<div class="form-group"><input type="text" id="noteEditorTags" placeholder="Tags (comma-separated)" class="notes-tags-input"></div>
</div>
<div class="form-group"><div id="noteEditorContentContainer" class="notes-content-input"></div></div>
<div class="notes-editor-actions">
<button class="btn-small btn-primary" id="noteSaveBtn">Save</button>
<button class="btn-small" id="noteCancelEditBtn" style="display:none">Cancel</button>
<button class="btn-small btn-danger" id="noteDeleteBtn" style="display:none">Delete</button>
</div>
</div>
</div>
</div>
</div>
</div>
</aside>
<div class="side-panel-overlay" id="sidePanelOverlay"></div>
</div><!-- .app-body -->
<!-- Environment Banner (bottom) -->
<div class="banner banner-bottom" id="bannerBottom"></div>
</div>
<!-- ── Auth Splash ─────────────────────────── -->
<div class="splash" id="splashGate">
<div class="splash-hero">
<div class="hero-content">
<div class="hero-logo-row">
<div class="hero-logo-mark" id="brandLogo">
<img src="favicon-256.png" alt="Chat Switchboard" style="width:100%;height:100%;border-radius:12px">
</div>
<div class="hero-wordmark" id="brandWordmark">Chat <span>Switchboard</span></div>
</div>
<h1 class="hero-headline" id="brandHeadline">One interface.<br>Every AI model.</h1>
<p class="hero-sub" id="brandTagline">Route conversations to OpenAI, Anthropic, and any OpenAI-compatible provider from a single, self-hosted chat interface. Bring your own keys. Keep your data.</p>
<div class="hero-features" id="brandPills">
<div class="hero-pill accent"><span class="pill-icon"></span> Streaming responses</div>
<div class="hero-pill purple"><span class="pill-icon"><img src="favicon-32.png" style="width:14px;height:14px;vertical-align:middle" alt=""></span> Multi-provider routing</div>
<div class="hero-pill"><span class="pill-icon">🔑</span> Bring your own API keys</div>
<div class="hero-pill"><span class="pill-icon">🏠</span> Self-hosted</div>
<div class="hero-pill"><span class="pill-icon">✈️</span> Air-gap ready</div>
<div class="hero-pill"><span class="pill-icon">🧠</span> Thinking blocks</div>
</div>
<div class="hero-version">v%%APP_VERSION%%</div>
</div>
</div>
<div class="splash-auth">
<div class="auth-card">
<div class="auth-card-header">
<h2>Welcome back</h2>
<p>Sign in to continue to your workspace</p>
</div>
<div class="auth-tabs">
<button class="auth-tab active" id="authTabLogin" onclick="switchAuthTab('login')">Sign In</button>
<button class="auth-tab" id="authTabRegister" onclick="switchAuthTab('register')">Register</button>
</div>
<div id="authLoginForm">
<div class="form-group"><label>Username or Email</label><input type="text" id="authLogin" autocomplete="username" placeholder="you@example.com"></div>
<div class="form-group"><label>Password</label><input type="password" id="authPassword" autocomplete="current-password" placeholder="••••••••" onkeydown="if(event.key==='Enter')handleLogin()"></div>
</div>
<div id="authRegisterForm" style="display:none">
<div class="form-group"><label>Username</label><input type="text" id="authUsername" autocomplete="username" placeholder="Choose a username"></div>
<div class="form-group"><label>Email</label><input type="email" id="authEmail" autocomplete="email" placeholder="you@example.com"></div>
<div class="form-group"><label>Password</label><input type="password" id="authRegPassword" autocomplete="new-password" placeholder="Min 8 characters" onkeydown="if(event.key==='Enter')handleRegister()"></div>
</div>
<div class="auth-error" id="authError"></div>
<div class="splash-error" id="splashError"></div>
<div class="auth-actions">
<button class="btn-primary btn-full" id="authLoginBtn" data-label="Sign In" onclick="handleLogin()">Sign In</button>
<button class="btn-primary btn-full" id="authRegisterBtn" data-label="Create Account" onclick="handleRegister()" style="display:none">Create Account</button>
</div>
<div class="auth-footer">
<p id="brandAuthFooter">Self-hosted AI chat &middot; Your keys, your data</p>
</div>
</div>
</div>
</div>
<!-- ── Settings Modal ──────────────────────── -->
<div class="modal-overlay" id="settingsModal">
<div class="modal">
<div class="modal-header"><h2>Settings</h2><button class="modal-close" id="settingsCloseBtn"></button></div>
<div class="modal-tabs settings-tabs">
<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="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="personas" onclick="UI.switchSettingsTab('personas')" id="settingsPersonasTabBtn">Personas</button>
<button class="settings-tab" data-stab="usage" onclick="UI.switchSettingsTab('usage')" id="settingsUsageTabBtn">Usage</button>
<button class="settings-tab" data-stab="roles" onclick="UI.switchSettingsTab('roles')" id="settingsRolesTabBtn">Model Roles</button>
<button class="settings-tab" data-stab="knowledgeBases" onclick="UI.switchSettingsTab('knowledgeBases')" id="settingsKBTabBtn" style="display:none">Knowledge</button>
<button class="settings-tab" data-stab="memory" onclick="UI.switchSettingsTab('memory')" id="settingsMemoryTabBtn">Memory</button>
<button class="settings-tab" data-stab="notifPrefs" onclick="UI.switchSettingsTab('notifPrefs')">Notifications</button>
</div>
<div class="modal-body">
<!-- General Tab -->
<div class="settings-tab-content" id="settingsGeneralTab">
<section class="settings-section">
<h3>Profile</h3>
<div class="avatar-upload-row">
<div class="avatar-preview" id="avatarPreview">
<span id="avatarPreviewLetter">?</span>
</div>
<div class="avatar-actions">
<button class="btn-small" id="avatarUploadBtn">Upload Photo</button>
<button class="btn-small btn-danger" id="avatarRemoveBtn" style="display:none">Remove</button>
<input type="file" id="avatarFileInput" accept="image/png,image/jpeg,image/gif" style="display:none">
<div class="form-hint">Auto-resized to 128×128</div>
</div>
</div>
<div class="form-group"><label>Display Name</label><input type="text" id="profileDisplayName"></div>
<div class="form-group"><label>Email</label><input type="email" id="profileEmail"></div>
<button class="btn-small" id="profileChangePwBtn">Change Password</button>
<div id="profileChangePwForm" style="display:none">
<div class="form-group"><label>Current Password</label><input type="password" id="profileCurrentPw"></div>
<div class="form-group"><label>New Password</label><input type="password" id="profileNewPw"></div>
<button class="btn-small btn-primary" id="profileSavePwBtn">Save Password</button>
</div>
</section>
<section class="settings-section" id="settingsTeamsSection" style="display:none">
<h3>My Teams</h3>
<div id="settingsTeamsList"></div>
</section>
<section class="settings-section">
<h3>Chat</h3>
<div class="form-group"><label>Default Model</label><select id="settingsModel"></select></div>
<div class="form-group"><label>System Prompt</label><textarea id="settingsSystemPrompt" rows="3"></textarea></div>
<div class="form-row">
<div class="form-group">
<label>Max Tokens <span class="form-hint" id="settingsMaxHint"></span></label>
<input type="number" id="settingsMaxTokens" placeholder="Auto (from model)">
</div>
<div class="form-group"><label>Temperature</label><input type="number" id="settingsTemp" value="0.7" step="0.1" min="0" max="2"></div>
</div>
<label class="checkbox-label"><input type="checkbox" id="settingsThinking" checked> Auto-expand thinking blocks</label>
</section>
</div>
<!-- Appearance Tab -->
<div class="settings-tab-content" id="settingsAppearanceTab" style="display:none">
<section class="settings-section">
<h3>Theme</h3>
<div class="form-group">
<label>Mode</label>
<div class="theme-toggle" id="themeToggle">
<button class="theme-btn" data-theme="light" title="Light">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
Light
</button>
<button class="theme-btn" data-theme="dark" title="Dark">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
Dark
</button>
<button class="theme-btn" data-theme="system" title="System">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
System
</button>
</div>
</div>
</section>
<section class="settings-section">
<h3>Display</h3>
<div class="form-group">
<label>UI Scale <span class="form-hint" id="scaleValue">100%</span></label>
<input type="range" id="settingsScale" min="80" max="150" step="5" value="100" class="range-input">
<div class="range-labels"><span>80%</span><span>150%</span></div>
</div>
<div class="form-group">
<label>Message Font Size <span class="form-hint" id="msgFontValue">14px</span></label>
<input type="range" id="settingsMsgFont" min="12" max="20" step="1" value="14" class="range-input">
<div class="range-labels"><span>12px</span><span>20px</span></div>
</div>
</section>
<section class="settings-section">
<h3>Editor</h3>
<div class="form-group">
<label>Keybindings</label>
<div class="theme-toggle" id="keymapToggle">
<button class="theme-btn" data-keymap="standard" title="Standard keybindings">Standard</button>
<button class="theme-btn" data-keymap="vim" title="Vim keybindings">Vim</button>
<button class="theme-btn" data-keymap="emacs" title="Emacs keybindings">Emacs</button>
</div>
<span class="form-hint">Applies to code editors only — chat input always uses standard.</span>
</div>
</section>
</div>
<!-- Providers Tab -->
<div class="settings-tab-content" id="settingsProvidersTab" style="display:none">
<section class="settings-section">
<div id="providerList"></div>
<button class="btn-small" id="providerShowAddBtn">+ Add Provider</button>
<div id="providerAddForm" style="display:none">
</div>
</section>
<div id="userProvidersDisabled" class="settings-notice" style="display:none">
<span>⚠️</span> User-configured providers have been disabled by the administrator.
</div>
</div>
<!-- Models Tab -->
<div class="settings-tab-content" id="settingsModelsTab" style="display:none">
<section class="settings-section">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
<h3 style="font-size:14px;margin:0">Available Models</h3>
<div>
<button class="btn-small" onclick="bulkSetUserModelVisibility(true)" title="Show all models in selector">Show All</button>
<button class="btn-small" onclick="bulkSetUserModelVisibility(false)" title="Hide all models from selector">Hide All</button>
</div>
</div>
<p class="section-hint" style="margin-bottom:8px">Toggle visibility in your model selector. Hidden models are still accessible through personas.</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 Personas</h3>
<p class="section-hint" style="margin-bottom:8px">Personal model personas 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>
<div id="userPresetKBPicker" style="display:none;margin-top:8px" class="admin-inline-form"></div>
<button class="btn-small" id="userAddPresetBtn" style="margin-top:6px">+ New Persona</button>
</section>
</div>
<!-- Usage Tab -->
<div class="settings-tab-content" id="settingsUsageTab" style="display:none">
<section class="settings-section">
<h3 style="font-size:14px;margin-bottom:6px">My Usage</h3>
<p class="section-hint" style="margin-bottom:8px">Token consumption and estimated costs across all your conversations.</p>
<div class="form-row" style="gap:6px;margin-bottom:8px">
<select id="myUsagePeriod" style="font-size:12px;padding:3px 6px" onchange="UI.loadMyUsage()">
<option value="7d">7 days</option>
<option value="30d" selected>30 days</option>
<option value="90d">90 days</option>
</select>
<select id="myUsageGroupBy" style="font-size:12px;padding:3px 6px" onchange="UI.loadMyUsage()">
<option value="model">By Model</option>
<option value="day">By Day</option>
</select>
</div>
<div id="myUsageTotals"></div>
<div id="myUsageResults"></div>
</section>
</div>
<!-- Model Roles Tab (BYOK users — personal role overrides) -->
<div class="settings-tab-content" id="settingsRolesTab" style="display:none">
<section class="settings-section">
<h3 style="font-size:14px;margin-bottom:4px">Personal Model Roles</h3>
<p class="section-hint" style="margin-bottom:8px">Override the org's default utility and embedding models with your own providers. Uses your personal API keys — no org cost.</p>
<div id="userRolesConfig"></div>
</section>
<div id="userRolesDisabled" class="settings-notice" style="display:none">
<span></span> Add a personal provider first to configure role overrides.
</div>
</div>
<div class="settings-tab-content" id="settingsKnowledgeBasesTab" style="display:none">
<div id="kbManagePanel"></div>
</div>
<!-- Memory Tab (v0.18.0) -->
<div class="settings-tab-content" id="settingsMemoryTab" style="display:none">
<section class="settings-section">
<h3 style="font-size:14px;margin-bottom:4px">My Memories</h3>
<p class="section-hint" style="margin-bottom:8px">Facts and preferences learned from your conversations. Memories help AI provide more personalized responses.</p>
<div id="settingsMemoryContent"></div>
</section>
</div>
<!-- Notification Preferences Tab (v0.20.0 Phase 3) -->
<div class="settings-tab-content" id="settingsNotifPrefsTab" style="display:none">
<section class="settings-section">
<h3>Delivery Preferences</h3>
<p class="section-hint" style="margin-bottom:8px">Choose how you receive notifications. Email delivery requires admin SMTP configuration.</p>
<div id="notifPrefsBody"></div>
</section>
</div>
</div>
<div class="modal-footer"><button class="btn-primary" id="settingsSaveBtn">Save Settings</button></div>
</div>
</div>
<!-- ── 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')">Personas</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>
<button class="admin-tab" data-ttab="knowledge" onclick="UI.switchTeamTab('knowledge')">Knowledge</button>
<button class="admin-tab" data-ttab="groups" onclick="UI.switchTeamTab('groups')">Groups</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 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="settingsTeamProviderForm" style="display:none;margin-top:8px" class="admin-inline-form">
</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>
<div id="teamPresetKBPicker" 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>
<!-- Knowledge tab -->
<div class="team-tab-content" id="teamTabKnowledge" style="display:none">
<div id="teamKBContent"></div>
</div>
<!-- Groups tab -->
<div class="team-tab-content" id="teamTabGroups" style="display:none">
<p class="text-muted" style="font-size:12px;margin:0 0 8px">Groups assigned to this team. Contact a system admin to create or modify groups.</p>
<div id="settingsTeamGroups"></div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Admin Panel (fullscreen overlay) ──── -->
<div class="admin-panel" id="adminPanel" style="display:none">
<div class="admin-topbar">
<button class="admin-back" id="adminBackBtn">← Chat</button>
<h1 class="admin-title">Admin Panel</h1>
<span class="admin-user" id="adminCurrentUser"></span>
</div>
<div class="admin-categories" id="adminCategories">
<button class="admin-cat active" data-cat="people">People</button>
<button class="admin-cat" data-cat="ai">AI</button>
<button class="admin-cat" data-cat="system">System</button>
<button class="admin-cat" data-cat="monitoring">Monitoring</button>
</div>
<div class="admin-body">
<nav class="admin-sidebar" id="adminSidebar"></nav>
<main class="admin-main" id="adminMain">
<!-- People -->
<div class="admin-section-content" id="adminUsersTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddUserBtn">+ Add User</button>
</div>
<div id="adminAddUserForm" style="display:none" class="admin-inline-form">
<div class="form-row">
<div class="form-group"><label>Username</label><input type="text" id="adminNewUsername" placeholder="username"></div>
<div class="form-group"><label>Email</label><input type="email" id="adminNewEmail" placeholder="email@example.com"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Password</label><input type="password" id="adminNewPassword" placeholder="min 8 chars"></div>
<div class="form-group"><label>Role</label><select id="adminNewRole"><option value="user">User</option><option value="admin">Admin</option></select></div>
</div>
<div class="form-row"><button class="btn-small btn-primary" id="adminCreateUserBtn">Create</button><button class="btn-small" id="adminCancelUserBtn">Cancel</button></div>
</div>
<div id="adminUserList"></div>
</div>
<div class="admin-section-content" id="adminTeamsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddTeamBtn">+ New Team</button>
</div>
<div id="adminAddTeamForm" style="display:none" class="admin-inline-form">
<div class="form-row">
<div class="form-group"><label>Team Name</label><input type="text" id="adminTeamName" placeholder="Engineering"></div>
</div>
<div class="form-group"><label>Description</label><input type="text" id="adminTeamDesc" placeholder="Core engineering team"></div>
<div class="form-row"><button class="btn-small btn-primary" id="adminCreateTeamBtn">Create</button><button class="btn-small" id="adminCancelTeamBtn">Cancel</button></div>
</div>
<div id="adminTeamList"></div>
<div id="adminTeamDetail" style="display:none">
<button class="notes-back-btn" id="adminTeamBackBtn">← Back to teams</button>
<h3 id="adminTeamDetailName" style="margin:8px 0 12px;font-size:15px"></h3>
<section class="settings-section" style="margin-bottom:12px;padding:10px;background:rgba(255,255,255,0.03);border-radius:8px">
<label class="checkbox-label"><input type="checkbox" id="adminTeamPrivatePolicy"> Require private providers (data stays on-prem)</label>
<p class="section-hint" style="margin:4px 0 0">Team members can only use providers marked as "private".</p>
<label class="checkbox-label" style="margin-top:8px"><input type="checkbox" id="adminTeamAllowProviders" checked> Allow team admins to manage providers</label>
<p class="section-hint" style="margin:4px 0 0">When disabled, team admins cannot add or modify team providers.</p>
</section>
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddMemberBtn">+ Add Member</button>
</div>
<div id="adminAddMemberForm" style="display:none" class="admin-inline-form">
<div class="form-row">
<div class="form-group"><label>User</label><select id="adminMemberUser"></select></div>
<div class="form-group"><label>Team Role</label><select id="adminMemberRole"><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="adminAddMemberSubmit">Add</button><button class="btn-small" id="adminCancelMemberBtn">Cancel</button></div>
</div>
<div id="adminMemberList"></div>
</div>
</div>
<div class="admin-section-content" id="adminGroupsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddGroupBtn">+ New Group</button>
</div>
<div id="adminAddGroupForm" style="display:none" class="admin-inline-form">
<div class="form-row">
<div class="form-group"><label>Group Name</label><input type="text" id="adminGroupName" placeholder="Engineering"></div>
<div class="form-group"><label>Scope</label>
<select id="adminGroupScope">
<option value="global">Global</option>
<option value="team">Team</option>
</select>
</div>
</div>
<div class="form-group" id="adminGroupTeamRow" style="display:none">
<label>Team</label><select id="adminGroupTeam"><option value="">Select team...</option></select>
</div>
<div class="form-group"><label>Description</label><input type="text" id="adminGroupDesc" placeholder="Access group for engineering resources"></div>
<div class="form-row"><button class="btn-small btn-primary" id="adminCreateGroupBtn">Create</button><button class="btn-small" id="adminCancelGroupBtn">Cancel</button></div>
</div>
<div id="adminGroupList"></div>
<div id="adminGroupDetail" style="display:none">
<button class="notes-back-btn" id="adminGroupBackBtn">← Back to groups</button>
<h3 id="adminGroupDetailName" style="margin:8px 0 4px;font-size:15px"></h3>
<p id="adminGroupDetailMeta" class="text-muted" style="font-size:12px;margin:0 0 12px"></p>
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddGroupMemberBtn">+ Add Member</button>
</div>
<div id="adminAddGroupMemberForm" style="display:none" class="admin-inline-form">
<div class="form-row">
<div class="form-group"><label>User</label><select id="adminGroupMemberUser"></select></div>
</div>
<div class="form-row"><button class="btn-small btn-primary" id="adminAddGroupMemberSubmit">Add</button><button class="btn-small" id="adminCancelGroupMemberBtn">Cancel</button></div>
</div>
<div id="adminGroupMemberList"></div>
</div>
</div>
<div class="admin-section-content" id="adminRolesTab" style="display:none">
<p style="color:var(--text-muted);margin:0 0 12px">Configure system model roles. Each role has a primary and optional fallback model.</p>
<div id="adminRolesContent">
<div class="loading">Loading roles...</div>
</div>
</div>
<!-- AI -->
<div class="admin-section-content" id="adminProvidersTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddProviderBtn">+ Add Global Provider</button>
</div>
<div id="adminAddProviderForm" style="display:none" class="admin-inline-form">
</div>
<div id="adminProviderList"></div>
</div>
<div class="admin-section-content" id="adminModelsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminFetchModelsBtn">⟳ Fetch Models</button>
<button class="btn-small" onclick="bulkSetVisibility('enabled')">All Enabled</button>
<button class="btn-small" onclick="bulkSetVisibility('team')">All Team</button>
<button class="btn-small" onclick="bulkSetVisibility('disabled')">All Disabled</button>
<span class="admin-hint" id="adminModelsHint"></span>
</div>
<div id="adminModelList"></div>
</div>
<div class="admin-section-content" id="adminPresetsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddPresetBtn">+ New Global Persona</button>
</div>
<div id="adminAddPresetForm" style="display:none" class="admin-inline-form"></div>
<div id="adminPresetKBPicker" style="display:none;margin-top:8px" class="admin-inline-form"></div>
<div id="adminPresetList"></div>
</div>
<div class="admin-section-content" id="adminKnowledgeBasesTab" style="display:none">
<div id="adminKBContent"></div>
</div>
<div class="admin-section-content" id="adminMemoryTab" style="display:none">
<div id="adminMemoryContent"></div>
</div>
<!-- System -->
<div class="admin-section-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>
<div class="form-group" style="margin-top: 8px;">
<label>Default state for new accounts</label>
<select id="adminRegDefaultState">
<option value="active">Active — immediate access</option>
<option value="pending">Pending — require admin approval</option>
</select>
</div>
</section>
<section class="settings-section">
<h3>User Providers</h3>
<label class="checkbox-label"><input type="checkbox" id="adminUserProvidersToggle" checked> Allow users to configure their own API providers</label>
<p class="section-hint">When disabled, users can only use admin-configured global providers.</p>
</section>
<section class="settings-section">
<h3>User Personas</h3>
<label class="checkbox-label"><input type="checkbox" id="adminUserPresetsToggle" checked> Allow users to create personal personas</label>
<p class="section-hint">When disabled, users can only use admin-created global personas.</p>
</section>
<section class="settings-section">
<h3>Knowledge Base Access</h3>
<label class="checkbox-label"><input type="checkbox" id="adminKBDirectAccessToggle"> Restrict KB access to persona bindings only</label>
<p class="section-hint">When enabled, users cannot attach KBs directly to channels — they can only use KBs bound to personas by admins.</p>
</section>
<section class="settings-section">
<h3>Default Model</h3>
<div class="form-group">
<select id="adminDefaultModel">
<option value="">— None (first visible) —</option>
</select>
</div>
<p class="section-hint">Model selected by default for new users or when a saved model is no longer available.</p>
</section>
<section class="settings-section">
<h3>Environment Banner</h3>
<label class="checkbox-label"><input type="checkbox" id="adminBannerEnabled"> Show environment banner</label>
<div id="bannerConfigFields" style="display:none">
<div class="form-group">
<label>Preset</label>
<select id="adminBannerPreset"><option value="">Custom</option></select>
</div>
<div class="form-group"><label>Banner Text</label><input type="text" id="adminBannerText" placeholder="DEVELOPMENT"></div>
<div class="form-row">
<div class="form-group"><label>Position</label>
<select id="adminBannerPosition"><option value="both">Top &amp; Bottom</option><option value="top">Top only</option><option value="bottom">Bottom only</option></select>
</div>
</div>
<div class="form-row">
<div class="form-group"><label>Background</label><div class="color-input-wrap"><input type="color" id="adminBannerBg" value="#007a33"><input type="text" id="adminBannerBgHex" class="color-hex" value="#007a33" maxlength="7"></div></div>
<div class="form-group"><label>Text Color</label><div class="color-input-wrap"><input type="color" id="adminBannerFg" value="#ffffff"><input type="text" id="adminBannerFgHex" class="color-hex" value="#ffffff" maxlength="7"></div></div>
</div>
<div class="banner-preview" id="bannerPreview">PREVIEW</div>
</div>
</section>
<section class="settings-section">
<h3>Web Search</h3>
<div class="form-group">
<label>Search Provider</label>
<select id="adminSearchProvider">
<option value="duckduckgo">DuckDuckGo (no API key)</option>
<option value="searxng">SearXNG (self-hosted)</option>
</select>
</div>
<div id="searxngConfigFields" style="display:none">
<div class="form-group"><label>SearXNG Endpoint</label><input type="text" id="adminSearchEndpoint" placeholder="https://search.internal.example.com"></div>
<div class="form-group"><label>API Key (optional)</label><input type="text" id="adminSearchApiKey" placeholder="Leave blank if not required"></div>
</div>
<div class="form-group">
<label>Max Results per Search</label>
<select id="adminSearchMaxResults">
<option value="3">3</option>
<option value="5" selected>5</option>
<option value="8">8</option>
<option value="10">10</option>
</select>
</div>
<p class="section-hint">Web search and URL fetch tools allow AI to look up current information. DuckDuckGo works out of the box. SearXNG is ideal for airgapped deployments.</p>
</section>
<section class="settings-section">
<h3>Auto-Compaction</h3>
<label class="checkbox-label"><input type="checkbox" id="adminCompactionEnabled"> Enable automatic conversation compaction</label>
<p class="section-hint">When enabled, long conversations are automatically summarized in the background using the utility model. Requires a utility model role to be configured.</p>
<div id="compactionConfigFields" style="display:none;margin-top:8px">
<div class="form-row">
<div class="form-group">
<label>Threshold</label>
<div class="form-row" style="gap:4px;align-items:center">
<input type="number" id="adminCompactionThreshold" value="70" min="10" max="95" step="5" style="width:70px">
<span class="form-hint">% of context window</span>
</div>
</div>
<div class="form-group">
<label>Cooldown</label>
<div class="form-row" style="gap:4px;align-items:center">
<input type="number" id="adminCompactionCooldown" value="30" min="5" max="360" step="5" style="width:70px">
<span class="form-hint">minutes per channel</span>
</div>
</div>
</div>
</div>
</section>
<section class="settings-section">
<h3>Memory Extraction</h3>
<label class="checkbox-label"><input type="checkbox" id="adminMemoryExtractionEnabled"> Enable automatic memory extraction</label>
<p class="section-hint">When enabled, the system automatically extracts memorable facts from conversations using the utility model. Extracted memories require approval before becoming active. Requires a utility model role.</p>
<div id="memoryExtractionConfigFields" style="display:none;margin-top:8px">
<label class="checkbox-label"><input type="checkbox" id="adminMemoryAutoApprove"> Auto-approve extracted memories</label>
<p class="section-hint">Skip the review pipeline and activate extracted memories immediately. Not recommended for sensitive environments.</p>
</div>
</section>
<section class="admin-section">
<h4>🔐 Encryption</h4>
<div id="adminVaultStatus"><span class="empty-hint">Loading…</span></div>
</section>
<section class="settings-section">
<h3>Email Notifications (SMTP)</h3>
<label class="checkbox-label"><input type="checkbox" id="adminEmailEnabled"> Enable email notifications</label>
<div id="smtpConfigFields" style="display:none">
<div class="smtp-config-grid" style="margin-top:12px">
<div class="form-group"><label>SMTP Host</label><input type="text" id="adminSmtpHost" placeholder="smtp.example.com"></div>
<div class="form-group"><label>Port</label><input type="number" id="adminSmtpPort" value="587" min="1" max="65535" style="width:100px"></div>
<div class="form-group"><label>Username</label><input type="text" id="adminSmtpUser" placeholder="user@example.com"></div>
<div class="form-group"><label>Password</label><input type="password" id="adminSmtpPassword" placeholder="••••••••"></div>
<div class="form-group"><label>From Address</label><input type="text" id="adminSmtpFrom" placeholder="noreply@chat.example.com"></div>
<div class="form-group"><label>TLS Mode</label>
<select id="adminSmtpTls">
<option value="false">STARTTLS (port 587)</option>
<option value="true">Implicit TLS (port 465)</option>
</select>
</div>
</div>
<div class="smtp-test-row">
<button class="btn-small" id="adminSmtpTestBtn" onclick="NotifPrefs._testEmail()">Send Test Email</button>
<span class="smtp-test-status" id="smtpTestStatus"></span>
</div>
<p class="section-hint" style="margin-top:8px">Test sends to your account's email address. Save settings first, then test.</p>
</div>
</section>
<button class="btn-primary btn-small" id="adminSaveSettings">Save Settings</button>
</div>
<div class="admin-section-content" id="adminStorageTab" style="display:none"><div id="adminStorageContent"></div></div>
<div class="admin-section-content" id="adminExtensionsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminInstallExtBtn">+ Install Extension</button>
</div>
<div id="adminExtensionsList"></div>
<div id="adminInstallExtForm" style="display:none;margin-top:12px" class="admin-inline-form">
<div class="form-row">
<div class="form-group"><label>Extension ID</label><input type="text" id="extInstallId" placeholder="my-extension"></div>
<div class="form-group"><label>Name</label><input type="text" id="extInstallName" placeholder="My Extension"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Version</label><input type="text" id="extInstallVersion" placeholder="1.0.0" value="1.0.0"></div>
<div class="form-group"><label>Author</label><input type="text" id="extInstallAuthor"></div>
</div>
<div class="form-group"><label>Description</label><input type="text" id="extInstallDesc"></div>
<div class="form-group"><label>Manifest JSON <span class="text-muted">(optional)</span></label><textarea id="extInstallManifest" rows="4" placeholder='{"tools":[], "permissions":[]}'></textarea></div>
<div class="form-group"><label>Script <span class="text-muted">(JavaScript source)</span></label><textarea id="extInstallScript" rows="6" placeholder="Extensions.register({ id: 'my-ext', init(ctx) { ... } });"></textarea></div>
<div class="form-row">
<label class="toggle-label"><input type="checkbox" id="extInstallSystem"> System (users can't disable)</label>
<label class="toggle-label"><input type="checkbox" id="extInstallEnabled" checked> Enabled</label>
</div>
<div class="form-actions">
<button class="btn-small" onclick="document.getElementById('adminInstallExtForm').style.display='none'">Cancel</button>
<button class="btn-small btn-primary" id="adminInstallExtSubmit">Install</button>
</div>
</div>
</div>
<!-- Monitoring -->
<div class="admin-section-content" id="adminUsageTab" style="display:none">
<div class="admin-toolbar" style="margin-bottom:12px">
<select id="usagePeriod" style="min-width:100px">
<option value="7d">Last 7 days</option>
<option value="30d" selected>Last 30 days</option>
<option value="90d">Last 90 days</option>
</select>
<select id="usageGroupBy" style="min-width:100px">
<option value="model">By Model</option>
<option value="user">By User</option>
<option value="day">By Day</option>
<option value="provider">By Provider</option>
</select>
<button class="btn-secondary btn-small" id="usageRefreshBtn">Refresh</button>
</div>
<div id="adminUsageTotals"></div>
<div id="adminUsageResults"></div>
<h3 style="margin-top:16px;font-size:14px">Model Pricing</h3>
<div id="adminPricingTable"></div>
</div>
<div class="admin-section-content" id="adminAuditTab" style="display:none">
<div class="admin-toolbar" style="margin-bottom:8px">
<select id="auditFilterAction" style="min-width:140px"><option value="">All actions</option></select>
<select id="auditFilterResource" style="min-width:120px">
<option value="">All types</option>
<option value="user">user</option>
<option value="team">team</option>
<option value="preset">preset</option>
<option value="channel">channel</option>
<option value="config">config</option>
</select>
<button class="btn-small" id="auditRefreshBtn">Refresh</button>
</div>
<div id="adminAuditList"></div>
<div class="pagination-row" id="auditPagination" style="display:none;margin-top:8px">
<button class="btn-small" id="auditPrevBtn">← Prev</button>
<span id="auditPageInfo" style="font-size:12px;color:var(--text-3)"></span>
<button class="btn-small" id="auditNextBtn">Next →</button>
</div>
</div>
<div class="admin-section-content" id="adminStatsTab" style="display:none"><div id="adminStats"></div></div>
</main>
</div>
</div>
<!-- ── Notes Modal ────────────────────────── -->
<!-- Debug Modal (Ctrl+Shift+L) -->
<div class="modal-overlay" id="debugModal">
<div class="modal debug-modal">
<div class="modal-header"><h2>Debug Log</h2><button class="modal-close" onclick="closeDebugModal()"></button></div>
<div class="modal-tabs debug-tabs">
<button class="debug-tab active" data-tab="console" onclick="switchDebugTab('console')">Console (<span id="debugConsoleCount">0</span>)</button>
<button class="debug-tab" data-tab="network" onclick="switchDebugTab('network')">Network (<span id="debugNetworkCount">0</span>)</button>
<button class="debug-tab" data-tab="state" onclick="switchDebugTab('state')">State</button>
</div>
<div class="modal-body debug-modal-body">
<div class="debug-tab-content" id="debugConsoleTab">
<div class="debug-toolbar">
<label class="debug-check"><input type="checkbox" id="debugFilterErrors"> Errors only</label>
<label class="debug-check"><input type="checkbox" id="debugAutoScroll" checked> Auto-scroll</label>
</div>
<div id="debugConsoleContent" class="debug-content"></div>
</div>
<div class="debug-tab-content" id="debugNetworkTab" style="display:none"><div id="debugNetworkContent" class="debug-content"></div></div>
<div class="debug-tab-content" id="debugStateTab" style="display:none"><div id="debugStateContent" class="debug-content"></div></div>
</div>
<div class="modal-footer debug-footer">
<button class="btn-primary btn-small" onclick="runDebugDiagnostics()">Diagnostics</button>
<button class="btn-small btn-danger" onclick="purgeCache()">Purge Cache</button>
<div style="margin-left:auto;display:flex;gap:0.5rem">
<button class="btn-small" onclick="clearDebugLog()">Clear</button>
<button class="btn-small" onclick="copyDebugLog()">Copy</button>
<button class="btn-small" onclick="exportDebugLog()">Export</button>
</div>
</div>
</div>
</div>
<!-- Command Palette (Ctrl+K) -->
<!-- ── Save to Note Modal ──────────────────── -->
<div class="modal-overlay" id="saveToNoteModal" style="display:none">
<div class="modal modal-narrow">
<div class="modal-header"><h2>Save to Note</h2><button class="modal-close" onclick="closeSaveToNoteModal()"></button></div>
<div class="modal-body">
<div class="form-group">
<label>Title</label>
<input type="text" id="saveToNoteTitle" placeholder="Note title" class="notes-title-input">
</div>
<div class="form-row">
<div class="form-group">
<label>Folder</label>
<input type="text" id="saveToNoteFolder" placeholder="/ (root)" class="notes-folder-input">
</div>
<div class="form-group">
<label>Tags</label>
<input type="text" id="saveToNoteTags" placeholder="comma-separated" class="notes-tags-input">
</div>
</div>
<div class="form-group">
<label class="save-to-note-toggle">
<input type="checkbox" id="saveToNoteAppendToggle" onchange="_toggleAppendMode(this.checked)">
Append to existing note
</label>
</div>
<div id="saveToNoteAppendPicker" style="display:none" class="form-group">
<label>Target note</label>
<input type="text" id="saveToNoteAppendSearch" placeholder="Search notes..." class="notes-title-input" oninput="_searchAppendTarget(this.value)">
<div id="saveToNoteAppendResults" class="save-to-note-results"></div>
</div>
<div class="form-group">
<label>Preview</label>
<div id="saveToNotePreview" class="save-to-note-preview msg-text"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn-small" onclick="closeSaveToNoteModal()">Cancel</button>
<button class="btn-small btn-primary" id="saveToNoteConfirmBtn" onclick="_confirmSaveToNote()">Create Note</button>
</div>
</div>
</div>
<div class="cmd-palette-overlay" id="cmdPalette">
<div class="cmd-palette">
<div class="cmd-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="cmdInput" placeholder="Type a command…" autocomplete="off" spellcheck="false">
<kbd class="cmd-kbd">esc</kbd>
</div>
<div class="cmd-results" id="cmdResults"></div>
<div class="cmd-footer">
<span><kbd>↑↓</kbd> navigate</span>
<span><kbd></kbd> run</span>
<span><kbd>esc</kbd> close</span>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<!-- Vendor libs -->
<script src="vendor/marked.min.js" onerror="this.onerror=null;this.src='https://cdnjs.cloudflare.com/ajax/libs/marked/16.3.0/lib/marked.umd.min.js'"></script>
<script src="vendor/purify.min.js" onerror="this.onerror=null;this.src='https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.4/purify.min.js'"></script>
<script src="vendor/codemirror/codemirror.bundle.js?v=%%APP_VERSION%%" onerror="console.warn('[CM6] Bundle not available — falling back to textarea')"></script>
<script src="js/debug.js?v=%%APP_VERSION%%"></script>
<script src="js/events.js?v=%%APP_VERSION%%"></script>
<script src="js/extensions.js?v=%%APP_VERSION%%"></script>
<script src="js/api.js?v=%%APP_VERSION%%"></script>
<script src="js/panels.js?v=%%APP_VERSION%%"></script>
<script src="js/ui-format.js?v=%%APP_VERSION%%"></script>
<script src="js/ui-primitives.js?v=%%APP_VERSION%%"></script>
<script src="js/ui-core.js?v=%%APP_VERSION%%"></script>
<script src="js/ui-settings.js?v=%%APP_VERSION%%"></script>
<script src="js/ui-admin.js?v=%%APP_VERSION%%"></script>
<script src="js/tokens.js?v=%%APP_VERSION%%"></script>
<script src="js/notes.js?v=%%APP_VERSION%%"></script>
<script src="js/note-graph.js?v=%%APP_VERSION%%"></script>
<script src="js/attachments.js?v=%%APP_VERSION%%"></script>
<script src="js/tools-toggle.js?v=%%APP_VERSION%%"></script>
<script src="js/knowledge-ui.js?v=%%APP_VERSION%%"></script>
<script src="js/memory-ui.js?v=%%APP_VERSION%%"></script>
<script src="js/persona-kb.js?v=%%APP_VERSION%%"></script>
<script src="js/projects-ui.js?v=%%APP_VERSION%%"></script>
<script src="js/channel-models.js?v=%%APP_VERSION%%"></script>
<script src="js/notification-prefs.js?v=%%APP_VERSION%%"></script>
<script src="js/notifications.js?v=%%APP_VERSION%%"></script>
<script src="js/chat.js?v=%%APP_VERSION%%"></script>
<script src="js/settings-handlers.js?v=%%APP_VERSION%%"></script>
<script src="js/admin-handlers.js?v=%%APP_VERSION%%"></script>
<script src="js/app.js?v=%%APP_VERSION%%"></script>
<script>
// ── Service Worker Registration ─────────
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js').then(reg => {
console.log('SW registered, scope:', reg.scope);
// Check for updates periodically
setInterval(() => reg.update(), 60 * 60 * 1000);
}).catch(err => console.warn('SW registration failed:', err));
});
}
// ── PWA Install Prompt ──────────────────
let _deferredInstallPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
_deferredInstallPrompt = e;
_showInstallBanner();
});
function _showInstallBanner() {
if (document.getElementById('pwaInstallBanner')) return;
const banner = document.createElement('div');
banner.id = 'pwaInstallBanner';
banner.className = 'pwa-install-banner';
banner.innerHTML =
'<span>Install Chat Switchboard for a better experience</span>' +
'<button class="btn-small btn-primary" onclick="_installPWA()">Install</button>' +
'<button class="btn-small" onclick="this.parentElement.remove()">Dismiss</button>';
document.body.appendChild(banner);
}
function _installPWA() {
if (!_deferredInstallPrompt) return;
_deferredInstallPrompt.prompt();
_deferredInstallPrompt.userChoice.then(() => {
_deferredInstallPrompt = null;
document.getElementById('pwaInstallBanner')?.remove();
});
}
window.addEventListener('appinstalled', () => {
document.getElementById('pwaInstallBanner')?.remove();
_deferredInstallPrompt = null;
});
</script>
<!-- Image lightbox overlay -->
<div class="lightbox-overlay" id="lightbox">
<button class="lightbox-close" title="Close (Esc)"></button>
<img id="lightboxImg" alt="">
</div>
</body>
</html>