1240 lines
85 KiB
HTML
1240 lines
85 KiB
HTML
<!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 A–Z</option>
|
||
<option value="title_desc">Title Z–A</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 · 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 & 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>
|