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

525 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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=0.4.1">
</head>
<body>
<div id="appContainer" class="app-hidden" style="display:none">
<header class="header">
<h1>🔀 Chat Switchboard</h1>
<div class="header-actions">
<div class="connection-status offline" id="connectionStatus" title="Unmanaged mode">
<span class="status-dot"></span> Offline
</div>
<button class="btn btn-secondary" id="toggleSidebarBtn" title="Toggle Sidebar (Ctrl+B)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="3" x2="9" y2="21"></line>
</svg>
</button>
<div class="dropdown">
<button class="btn btn-secondary" id="exportBtn" title="Export Chat">
📥 Export
</button>
<div class="dropdown-content" id="exportDropdown">
<button class="dropdown-item" onclick="exportChat('markdown')">📄 Markdown</button>
<button class="dropdown-item" onclick="exportChat('json')">📋 JSON</button>
<button class="dropdown-item" onclick="exportChat('text')">📝 Plain Text</button>
</div>
</div>
<button class="btn btn-secondary admin-only" id="adminBtn" title="Admin Panel" style="display:none">
🛡️ Admin
</button>
<button class="btn btn-secondary" id="settingsBtn" title="Settings (Ctrl+,)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
Settings
</button>
</div>
</header>
<div class="main-container">
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<span class="sidebar-title">Chat History</span>
</div>
<button class="btn btn-primary new-chat-btn" id="newChatBtn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
New Chat
</button>
<div class="chat-history" id="chatHistory"></div>
</aside>
<main class="chat-area">
<div class="chat-messages" id="chatMessages">
<div class="empty-state" id="emptyState">
<div class="empty-state-icon">
<svg 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"></path>
</svg>
</div>
<h2>Start a Conversation</h2>
<p>Configure your API settings and start chatting</p>
<button class="btn btn-primary" style="margin-top: 1.5rem;" id="configureBtn">⚙️ Configure API</button>
</div>
</div>
<div class="input-area">
<div class="input-container">
<div class="input-top-bar">
<div class="model-selector">
<label for="quickModel">Model:</label>
<select id="quickModel">
<option value="">-- Configure in Settings --</option>
</select>
<button class="btn btn-small btn-secondary" id="quickFetchBtn" title="Refresh models">🔄</button>
</div>
<div class="input-shortcuts">
<kbd>Enter</kbd> send · <kbd>Shift+Enter</kbd> newline · <kbd>Esc</kbd> stop
</div>
</div>
<div class="input-wrapper">
<textarea id="messageInput" placeholder="Type your message..." rows="1"></textarea>
<div class="input-actions">
<div class="input-left">
<button class="btn btn-small btn-secondary" id="regenerateBtn" title="Regenerate last response" style="display: none;">🔄 Regenerate</button>
</div>
<div style="display: flex; gap: 0.5rem;">
<button class="stop-btn" id="stopBtn">⏹ Stop</button>
<button class="send-btn" id="sendBtn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
Send
</button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Settings Modal -->
<div class="modal-overlay" id="settingsModal">
<div class="modal">
<div class="modal-header">
<h2>⚙️ Settings</h2>
<button class="modal-close" id="closeModalBtn"></button>
</div>
<div class="modal-body">
<!-- Mode indicator -->
<div class="settings-mode-badge" id="settingsModeBadge"></div>
<!-- Profile (managed mode only) -->
<div class="settings-section" id="profileSection" style="display:none">
<h3 class="settings-section-title">Profile</h3>
<div class="form-group">
<label for="profileDisplayName">Display Name</label>
<input type="text" id="profileDisplayName" placeholder="How you want to be called">
</div>
<div class="form-group">
<label for="profileEmail">Email</label>
<input type="email" id="profileEmail" placeholder="you@example.com">
</div>
<div class="profile-password-row">
<button class="btn btn-secondary btn-small" id="profileChangePwBtn">🔑 Change Password</button>
</div>
<div id="profileChangePwForm" style="display:none">
<div class="form-group">
<label for="profileCurrentPw">Current Password</label>
<input type="password" id="profileCurrentPw" placeholder="••••••••">
</div>
<div class="form-group">
<label for="profileNewPw">New Password</label>
<input type="password" id="profileNewPw" placeholder="At least 8 characters">
</div>
<button class="btn btn-primary btn-small" id="profileSavePwBtn">Update Password</button>
</div>
<hr class="settings-divider">
</div>
<!-- API Settings (unmanaged / offline only) -->
<div id="unmanagedSettings">
<div class="form-group">
<label for="apiEndpoint">API Endpoint</label>
<input type="text" id="apiEndpoint" placeholder="https://api.openai.com/v1">
<small>Base URL for the OpenAI-compatible API</small>
</div>
<div class="form-group">
<label for="apiKey">API Key</label>
<input type="password" id="apiKey" placeholder="sk-...">
<small>Your API key for authentication</small>
</div>
</div> <!-- /unmanagedSettings -->
<!-- API Providers (managed mode only) -->
<div id="managedProviders" style="display:none">
<h3 class="settings-section-title">API Providers</h3>
<div id="providerList" class="provider-list"></div>
<div class="provider-add-toggle">
<button class="btn btn-secondary btn-small" id="providerShowAddBtn">+ Add Provider</button>
</div>
<div class="provider-add-form" id="providerAddForm" style="display:none">
<div class="form-group">
<label for="providerName">Name</label>
<input type="text" id="providerName" placeholder="e.g. My OpenAI Key">
</div>
<div class="form-group">
<label for="providerType">Provider</label>
<select id="providerType">
<option value="openai">OpenAI-compatible</option>
<option value="anthropic">Anthropic</option>
</select>
</div>
<div class="form-group">
<label for="providerEndpoint">Endpoint</label>
<input type="text" id="providerEndpoint" placeholder="https://api.openai.com/v1">
</div>
<div class="form-group">
<label for="providerApiKey">API Key</label>
<input type="password" id="providerApiKey" placeholder="sk-...">
</div>
<div class="form-group">
<label for="providerDefaultModel">Default Model (optional)</label>
<input type="text" id="providerDefaultModel" placeholder="gpt-4o">
</div>
<div class="admin-form-row">
<button class="btn btn-primary btn-small" id="providerCreateBtn">Save</button>
<button class="btn btn-secondary btn-small" id="providerCancelBtn">Cancel</button>
</div>
</div>
<hr class="settings-divider">
</div>
<div class="form-group">
<label for="model">Default Model</label>
<div style="display: flex; gap: 0.5rem;">
<select id="model" style="flex: 1;">
<option value="">-- Select or type below --</option>
</select>
<button type="button" class="btn btn-secondary" id="fetchModelsBtn" title="Fetch available models">
🔄 Fetch
</button>
</div>
<input type="text" id="modelCustom" placeholder="Or type custom model name..." style="margin-top: 0.5rem;">
<small>Select from list or enter custom model name</small>
</div>
<div class="toggle-group">
<span class="toggle-label">Stream Responses</span>
<label class="toggle-switch">
<input type="checkbox" id="streamResponse" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="toggle-group">
<span class="toggle-label">Save Chat History</span>
<label class="toggle-switch">
<input type="checkbox" id="saveHistory" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="toggle-group">
<span class="toggle-label">Show Thinking Blocks</span>
<label class="toggle-switch">
<input type="checkbox" id="showThinking" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="form-group">
<label for="systemPrompt">System Prompt (Optional)</label>
<textarea id="systemPrompt" rows="3" placeholder="You are a helpful AI assistant."></textarea>
<small>Instructions that define the AI's behavior</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="maxTokens">Max Tokens</label>
<input type="number" id="maxTokens" value="4096" min="1" max="32768">
</div>
<div class="form-group">
<label for="temperature">Temperature</label>
<input type="number" id="temperature" value="0.7" min="0" max="2" step="0.1">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="topP">Top P</label>
<input type="number" id="topP" value="1" min="0" max="1" step="0.1">
</div>
<div class="form-group">
<label for="presencePenalty">Presence Penalty</label>
<input type="number" id="presencePenalty" value="0" min="-2" max="2" step="0.1">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancelBtn">Cancel</button>
<button class="btn btn-primary" id="saveBtn">💾 Save Settings</button>
</div>
</div>
</div>
<!-- Admin Panel Modal -->
<div class="modal-overlay" id="adminModal">
<div class="modal admin-modal">
<div class="modal-header">
<h2>🛡️ Admin Panel</h2>
<button class="modal-close" id="adminCloseBtn"></button>
</div>
<div class="modal-body">
<div class="admin-tabs">
<button class="admin-tab active" data-tab="users">Users</button>
<button class="admin-tab" data-tab="models">Models</button>
<button class="admin-tab" data-tab="providers">Providers</button>
<button class="admin-tab" data-tab="settings">Settings</button>
<button class="admin-tab" data-tab="stats">Stats</button>
</div>
<!-- Users Tab -->
<div class="admin-tab-content active" id="adminUsersTab">
<div class="admin-add-user-toggle">
<button class="btn btn-primary btn-small" id="adminShowAddUser">+ Add User</button>
</div>
<div class="admin-add-user-form" id="adminAddUserForm" style="display:none">
<div class="admin-form-row">
<input type="text" id="adminNewUsername" placeholder="Username" class="admin-input">
<input type="email" id="adminNewEmail" placeholder="Email" class="admin-input">
</div>
<div class="admin-form-row">
<input type="password" id="adminNewPassword" placeholder="Password (min 8)" class="admin-input">
<select id="adminNewRole" class="admin-role-select">
<option value="user">user</option>
<option value="admin">admin</option>
<option value="moderator">moderator</option>
</select>
</div>
<div class="admin-form-row">
<button class="btn btn-primary btn-small" id="adminCreateUserBtn">Create</button>
<button class="btn btn-secondary btn-small" id="adminCancelAddUser">Cancel</button>
</div>
</div>
<div id="adminUserList" class="admin-user-list">Loading...</div>
</div>
<!-- Reset Password Dialog (inline overlay) -->
<div class="admin-reset-dialog" id="adminResetDialog" style="display:none">
<div class="admin-reset-card">
<h3>Reset Password</h3>
<p id="adminResetTarget"></p>
<input type="password" id="adminResetNewPw" placeholder="New password (min 8)" class="admin-input">
<div class="admin-form-row">
<button class="btn btn-primary btn-small" id="adminResetConfirmBtn">Reset</button>
<button class="btn btn-secondary btn-small" id="adminResetCancelBtn">Cancel</button>
</div>
</div>
</div>
<!-- Models Tab -->
<div class="admin-tab-content" id="adminModelsTab" style="display:none">
<div class="admin-models-header">
<p class="admin-hint">Manage which models are available to users. Fetch from configured providers, then enable/disable and tag capabilities.</p>
<button class="btn btn-primary btn-small" id="adminFetchModelsBtn">🔄 Fetch Models</button>
</div>
<div id="adminModelList" class="admin-model-list">
<div class="admin-empty">No models configured. Add a provider first, then fetch models.</div>
</div>
</div>
<!-- Providers Tab -->
<div class="admin-tab-content" id="adminProvidersTab" style="display:none">
<p class="admin-hint">Global providers are available to all users. Users can also add their own.</p>
<div id="adminProviderList" class="provider-list"></div>
<div class="provider-add-toggle">
<button class="btn btn-secondary btn-small" id="adminProviderShowAddBtn">+ Add Global Provider</button>
</div>
<div class="provider-add-form" id="adminProviderAddForm" style="display:none">
<div class="form-group">
<label for="adminProviderName">Name</label>
<input type="text" id="adminProviderName" placeholder="e.g. Shared OpenAI">
</div>
<div class="form-group">
<label for="adminProviderType">Provider</label>
<select id="adminProviderType">
<option value="openai">OpenAI-compatible</option>
<option value="anthropic">Anthropic</option>
</select>
</div>
<div class="form-group">
<label for="adminProviderEndpoint">Endpoint</label>
<input type="text" id="adminProviderEndpoint" placeholder="https://api.openai.com/v1">
</div>
<div class="form-group">
<label for="adminProviderApiKey">API Key</label>
<input type="password" id="adminProviderApiKey" placeholder="sk-...">
</div>
<div class="form-group">
<label for="adminProviderDefaultModel">Default Model (optional)</label>
<input type="text" id="adminProviderDefaultModel" placeholder="gpt-4o">
</div>
<div class="admin-form-row">
<button class="btn btn-primary btn-small" id="adminProviderCreateBtn">Save</button>
<button class="btn btn-secondary btn-small" id="adminProviderCancelBtn">Cancel</button>
</div>
</div>
</div>
<!-- Settings Tab -->
<div class="admin-tab-content" id="adminSettingsTab" style="display:none">
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="adminRegToggle" checked>
<span>Allow new user registration</span>
</label>
</div>
<div class="form-group">
<label for="adminSiteName">Site Name</label>
<input type="text" id="adminSiteName" value="Chat Switchboard">
</div>
<div class="form-group">
<label for="adminTagline">Tagline</label>
<input type="text" id="adminTagline" value="Multi-Model AI Chat">
</div>
<button class="btn btn-primary" id="adminSaveSettings">💾 Save</button>
</div>
<!-- Stats Tab -->
<div class="admin-tab-content" id="adminStatsTab" style="display:none">
<div id="adminStats" class="admin-stats">Loading...</div>
</div>
</div>
</div>
</div>
</div> <!-- /appContainer -->
<!-- ── Auth Splash Gate ────────────────── -->
<div class="splash-gate" id="splashGate" style="display:none">
<div class="splash-card">
<div class="splash-brand">
<div class="splash-logo">🔀</div>
<h1>Chat Switchboard</h1>
<p class="splash-tagline">Multi-Model AI Chat</p>
</div>
<div class="splash-form">
<div class="auth-tabs">
<button class="auth-tab active" id="authTabLogin">Sign In</button>
<button class="auth-tab" id="authTabRegister">Register</button>
</div>
<div id="authLoginForm">
<div class="form-group">
<label for="authLogin">Username or Email</label>
<input type="text" id="authLogin" placeholder="jeff or jeff@example.com" autocomplete="username">
</div>
<div class="form-group">
<label for="authPassword">Password</label>
<input type="password" id="authPassword" placeholder="••••••••" autocomplete="current-password">
</div>
</div>
<div id="authRegisterForm" style="display:none;">
<div class="form-group">
<label for="authUsername">Username</label>
<input type="text" id="authUsername" placeholder="Choose a username" autocomplete="username">
</div>
<div class="form-group">
<label for="authEmail">Email</label>
<input type="email" id="authEmail" placeholder="you@example.com" autocomplete="email">
</div>
<div class="form-group">
<label for="authRegPassword">Password</label>
<input type="password" id="authRegPassword" placeholder="At least 8 characters" autocomplete="new-password">
</div>
</div>
<div class="auth-error" id="authError"></div>
<div class="splash-actions">
<button class="btn btn-primary btn-full" id="authLoginBtn">Sign In</button>
<button class="btn btn-primary btn-full" id="authRegisterBtn" style="display:none;">Create Account</button>
<div class="splash-divider"><span>or</span></div>
<button class="btn btn-secondary btn-full" id="authSkipBtn">Use Offline</button>
</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="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">
<!-- Console Tab -->
<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>
<!-- Network Tab -->
<div class="debug-tab-content" id="debugNetworkTab" style="display:none">
<div id="debugNetworkContent" class="debug-content"></div>
</div>
<!-- State Tab -->
<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 btn-primary btn-small" onclick="runDebugDiagnostics()">🔍 Run Diagnostics</button>
<div style="margin-left: auto; display: flex; gap: 0.5rem;">
<button class="btn btn-secondary btn-small" onclick="clearDebugLog()">🗑️ Clear</button>
<button class="btn btn-secondary btn-small" onclick="copyDebugLog()">📋 Copy</button>
<button class="btn btn-secondary btn-small" onclick="exportDebugLog()">📥 Export</button>
</div>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/debug.js?v=0.4.1"></script>
<script src="js/storage.js?v=0.4.1"></script>
<script src="js/backend.js?v=0.4.1"></script>
<script src="js/state.js?v=0.4.1"></script>
<script src="js/api.js?v=0.4.1"></script>
<script src="js/ui.js?v=0.4.1"></script>
<script src="js/admin.js?v=0.4.1"></script>
<script src="js/app.js?v=0.4.1"></script>
</body>
</html>