562 lines
39 KiB
HTML
562 lines
39 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>try { window.__BRANDING__ = %%BRANDING_JSON%%; } catch(e) { window.__BRANDING__ = {}; }</script>
|
||
<title>Chat Switchboard</title>
|
||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png">
|
||
<link rel="apple-touch-icon" sizes="192x192" href="favicon-192.png">
|
||
<link rel="stylesheet" href="css/styles.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">🔀</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 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-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="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>
|
||
|
||
<div class="messages" id="chatMessages">
|
||
<div class="empty-state">
|
||
<div class="empty-logo">🔀</div>
|
||
<h2>Chat Switchboard</h2>
|
||
<p>Select a model and start chatting</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="input-area">
|
||
<div class="input-wrap">
|
||
<textarea id="messageInput" placeholder="Send a message..." rows="1"></textarea>
|
||
<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>
|
||
</main>
|
||
</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">
|
||
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||
<rect width="64" height="64" rx="12" fill="#1a1a2e"/>
|
||
<path d="M12 48 L36 16" stroke="#6c9fff" stroke-width="5.5" stroke-linecap="round" fill="none"/>
|
||
<polygon points="42,12 34,14 38,22" fill="#6c9fff"/>
|
||
<path d="M12 16 L24 30" stroke="#a78bfa" stroke-width="5.5" stroke-linecap="round" fill="none"/>
|
||
<path d="M30 38 L36 48" stroke="#a78bfa" stroke-width="5.5" stroke-linecap="round" fill="none"/>
|
||
<polygon points="42,52 34,50 38,42" fill="#a78bfa"/>
|
||
</svg>
|
||
</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">🔀</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')">Providers</button>
|
||
<button class="settings-tab" data-stab="models" onclick="UI.switchSettingsTab('models')">Models</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">
|
||
<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> Show thinking blocks</label>
|
||
</section>
|
||
</div>
|
||
<!-- Appearance Tab -->
|
||
<div class="settings-tab-content" id="settingsAppearanceTab" style="display:none">
|
||
<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>
|
||
</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 class="form-group"><label>Name</label><input type="text" id="providerName" placeholder="My OpenAI Key"></div>
|
||
<div class="form-group"><label>Provider</label><select id="providerType"><option value="openai">OpenAI-compatible</option><option value="anthropic">Anthropic</option><option value="venice">Venice.ai</option><option value="openrouter">OpenRouter</option></select></div>
|
||
<div class="form-group"><label>Endpoint</label><input type="text" id="providerEndpoint" placeholder="https://api.openai.com/v1"></div>
|
||
<div class="form-group"><label>API Key</label><input type="password" id="providerApiKey" placeholder="sk-..."></div>
|
||
<div class="form-group"><label>Default Model</label><input type="text" id="providerDefaultModel" placeholder="gpt-4o"></div>
|
||
<div class="form-row"><button class="btn-small btn-primary" id="providerCreateBtn">Save</button><button class="btn-small" id="providerCancelBtn">Cancel</button></div>
|
||
</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">
|
||
<p class="section-hint">Models available from your providers and global configs.</p>
|
||
<div id="userModelList" class="model-list-grid"><div class="empty-hint">Loading models...</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer"><button class="btn-primary" id="settingsSaveBtn">Save Settings</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Admin Modal ─────────────────────────── -->
|
||
<div class="modal-overlay" id="adminModal">
|
||
<div class="modal modal-wide">
|
||
<div class="modal-header"><h2>Admin Panel</h2><button class="modal-close" id="adminCloseBtn">✕</button></div>
|
||
<div class="modal-tabs admin-tabs">
|
||
<button class="admin-tab active" data-tab="users">Users</button>
|
||
<button class="admin-tab" data-tab="providers">Providers</button>
|
||
<button class="admin-tab" data-tab="models">Models</button>
|
||
<button class="admin-tab" data-tab="presets">Presets</button>
|
||
<button class="admin-tab" data-tab="settings">Settings</button>
|
||
<button class="admin-tab" data-tab="stats">Stats</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="admin-tab-content" id="adminUsersTab">
|
||
<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-tab-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 class="form-row">
|
||
<div class="form-group"><label>Name</label><input type="text" id="adminProvName" placeholder="OpenAI Production"></div>
|
||
<div class="form-group"><label>Provider</label><select id="adminProvType"><option value="openai">OpenAI-compatible</option><option value="anthropic">Anthropic</option><option value="venice">Venice.ai</option><option value="openrouter">OpenRouter</option></select></div>
|
||
</div>
|
||
<div class="form-group"><label>Endpoint</label><input type="text" id="adminProvEndpoint" placeholder="https://api.openai.com/v1"></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label>API Key</label><input type="password" id="adminProvKey" placeholder="sk-..."></div>
|
||
<div class="form-group"><label>Default Model</label><input type="text" id="adminProvModel" placeholder="gpt-4o"></div>
|
||
</div>
|
||
<div class="form-row"><button class="btn-small btn-primary" id="adminCreateProvBtn">Save</button><button class="btn-small" id="adminCancelProvBtn">Cancel</button></div>
|
||
</div>
|
||
<div id="adminProviderList"></div>
|
||
</div>
|
||
<div class="admin-tab-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="bulkToggleModels(true)">Enable All</button>
|
||
<button class="btn-small" onclick="bulkToggleModels(false)">Disable All</button>
|
||
<span class="admin-hint" id="adminModelsHint"></span>
|
||
</div>
|
||
<div id="adminModelList"></div>
|
||
</div>
|
||
<div class="admin-tab-content" id="adminPresetsTab" style="display:none">
|
||
<div class="admin-toolbar">
|
||
<button class="btn-small btn-primary" id="adminAddPresetBtn">+ New Global Preset</button>
|
||
</div>
|
||
<div id="adminAddPresetForm" style="display:none" class="admin-inline-form">
|
||
<div class="form-row">
|
||
<div class="form-group"><label>Name</label><input type="text" id="adminPresetName" placeholder="Code Reviewer"></div>
|
||
<div class="form-group"><label>Icon</label><input type="text" id="adminPresetIcon" placeholder="🔍" maxlength="4" style="width:60px"></div>
|
||
</div>
|
||
<div class="avatar-upload-row" style="margin-bottom:0.75rem">
|
||
<div class="avatar-preview avatar-preview-sm" id="presetAvatarPreview">
|
||
<span id="presetAvatarPlaceholder">🤖</span>
|
||
</div>
|
||
<div class="avatar-actions">
|
||
<button class="btn-small" type="button" id="presetAvatarUploadBtn">Upload Avatar</button>
|
||
<button class="btn-small btn-danger" type="button" id="presetAvatarRemoveBtn" style="display:none">Remove</button>
|
||
<input type="file" id="presetAvatarFileInput" accept="image/png,image/jpeg,image/gif" style="display:none">
|
||
<div class="form-hint">Optional · 128×128 · shown in chat</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group"><label>Description</label><input type="text" id="adminPresetDesc" placeholder="Reviews code for best practices"></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label>Base Model</label><select id="adminPresetModel"></select></div>
|
||
<div class="form-group"><label>Provider Config</label><select id="adminPresetConfig"><option value="">Auto-resolve</option></select></div>
|
||
</div>
|
||
<div class="form-group"><label>System Prompt</label><textarea id="adminPresetPrompt" rows="3" placeholder="You are a code reviewer..."></textarea></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label>Temperature</label><input type="number" id="adminPresetTemp" step="0.1" min="0" max="2" placeholder="default"></div>
|
||
<div class="form-group"><label>Max Tokens</label><input type="number" id="adminPresetMaxTokens" placeholder="default"></div>
|
||
</div>
|
||
<div class="form-row"><button class="btn-small btn-primary" id="adminCreatePresetBtn">Create</button><button class="btn-small" id="adminCancelPresetBtn">Cancel</button></div>
|
||
</div>
|
||
<div id="adminPresetList"></div>
|
||
</div>
|
||
<div class="admin-tab-content" id="adminSettingsTab" style="display:none">
|
||
<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>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>
|
||
<button class="btn-primary btn-small" id="adminSaveSettings">Save Settings</button>
|
||
</div>
|
||
<div class="admin-tab-content" id="adminStatsTab" style="display:none"><div id="adminStats"></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Notes Modal ────────────────────────── -->
|
||
<div class="modal-overlay" id="notesModal">
|
||
<div class="modal modal-wide">
|
||
<div class="modal-header">
|
||
<h2>Notes</h2>
|
||
<div class="notes-header-actions">
|
||
<button class="btn-small" id="notesSelectModeBtn">Select</button>
|
||
<button class="btn-small btn-primary" id="notesNewBtn">+ New Note</button>
|
||
<button class="modal-close" onclick="closeModal('notesModal')">✕</button>
|
||
</div>
|
||
</div>
|
||
<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-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>
|
||
<div class="modal-body notes-body">
|
||
<!-- 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>
|
||
<!-- 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>
|
||
<div class="notes-editor-actions">
|
||
<button class="btn-small btn-primary" id="noteEditBtn2">Edit</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"><textarea id="noteEditorContent" rows="12" placeholder="Note content (Markdown supported)..." class="notes-content-input"></textarea></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>
|
||
</div>
|
||
|
||
<!-- 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>
|
||
<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>
|
||
|
||
<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="js/debug.js?v=%%APP_VERSION%%"></script>
|
||
<script src="js/events.js?v=%%APP_VERSION%%"></script>
|
||
<script src="js/api.js?v=%%APP_VERSION%%"></script>
|
||
<script src="js/ui.js?v=%%APP_VERSION%%"></script>
|
||
<script src="js/app.js?v=%%APP_VERSION%%"></script>
|
||
</body>
|
||
</html>
|