// ========================================== // UI Functions // ========================================== function updateSettingsUI() { document.getElementById('apiEndpoint').value = State.settings.apiEndpoint || ''; document.getElementById('apiKey').value = State.settings.apiKey || ''; const modelSelect = document.getElementById('model'); const modelCustom = document.getElementById('modelCustom'); const savedModel = State.settings.model || 'gpt-3.5-turbo'; populateModelSelect(modelSelect); const optionExists = Array.from(modelSelect.options).some(opt => opt.value === savedModel); if (optionExists) { modelSelect.value = savedModel; modelCustom.value = ''; } else { modelSelect.value = ''; modelCustom.value = savedModel; } document.getElementById('streamResponse').checked = State.settings.stream; document.getElementById('saveHistory').checked = State.settings.saveHistory; document.getElementById('showThinking').checked = State.settings.showThinking; document.getElementById('systemPrompt').value = State.settings.systemPrompt || ''; document.getElementById('maxTokens').value = State.settings.maxTokens; document.getElementById('temperature').value = State.settings.temperature; document.getElementById('topP').value = State.settings.topP; document.getElementById('presencePenalty').value = State.settings.presencePenalty; } function populateModelSelect(selectElement) { const currentValue = selectElement.value; selectElement.innerHTML = ''; State.models.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.id + (model.owned_by ? ` (${model.owned_by})` : ''); selectElement.appendChild(option); }); if (currentValue) { selectElement.value = currentValue; } } function updateQuickModelSelector() { const quickSelect = document.getElementById('quickModel'); const currentModel = State.settings.model; quickSelect.innerHTML = ''; if (State.models.length === 0) { quickSelect.innerHTML = ''; if (currentModel) { const opt = document.createElement('option'); opt.value = currentModel; opt.textContent = currentModel; quickSelect.appendChild(opt); quickSelect.value = currentModel; } return; } State.models.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.id; quickSelect.appendChild(option); }); if (currentModel) { const exists = Array.from(quickSelect.options).some(opt => opt.value === currentModel); if (exists) { quickSelect.value = currentModel; } else { const opt = document.createElement('option'); opt.value = currentModel; opt.textContent = currentModel + ' (custom)'; quickSelect.insertBefore(opt, quickSelect.firstChild); quickSelect.value = currentModel; } } } function openSettings() { updateSettingsUI(); updateProfileSection(); updateProviderSection(); document.getElementById('settingsModal').classList.add('active'); } function updateProfileSection() { const profileSection = document.getElementById('profileSection'); const unmanagedSettings = document.getElementById('unmanagedSettings'); const modeBadge = document.getElementById('settingsModeBadge'); if (Backend.isManaged) { profileSection.style.display = ''; unmanagedSettings.style.display = 'none'; modeBadge.textContent = '๐Ÿ”— Managed Mode โ€” API keys are configured by your admin or in Providers below'; modeBadge.className = 'settings-mode-badge mode-managed'; loadProfileIntoSettings(); } else { profileSection.style.display = 'none'; unmanagedSettings.style.display = ''; modeBadge.textContent = '๐Ÿ“ฑ Offline Mode โ€” Configure your own API endpoint and key'; modeBadge.className = 'settings-mode-badge mode-unmanaged'; } document.getElementById('profileChangePwForm').style.display = 'none'; } async function loadProfileIntoSettings() { try { const profile = await Backend.getProfile(); document.getElementById('profileDisplayName').value = profile.display_name || ''; document.getElementById('profileEmail').value = profile.email || ''; } catch (e) { console.warn('Failed to load profile:', e); } } async function saveProfile() { if (!Backend.isManaged) return; const displayName = document.getElementById('profileDisplayName').value.trim(); const email = document.getElementById('profileEmail').value.trim(); const updates = {}; if (displayName !== (Backend.user.display_name || '')) { updates.display_name = displayName || null; } if (email && email !== Backend.user.email) { updates.email = email; } if (Object.keys(updates).length === 0) return; try { const profile = await Backend.updateProfile(updates); // Update local user state if (profile.display_name !== undefined) Backend.user.display_name = profile.display_name; if (profile.email) Backend.user.email = profile.email; Backend.save(); showToast('โœ… Profile updated', 'success'); } catch (e) { showToast('โŒ ' + e.message, 'error'); } } async function handleChangePassword() { const current = document.getElementById('profileCurrentPw').value; const newPw = document.getElementById('profileNewPw').value; if (!current || !newPw) { showToast('โš ๏ธ Fill in both fields', 'warning'); return; } if (newPw.length < 8) { showToast('โš ๏ธ New password must be at least 8 characters', 'warning'); return; } try { await Backend.changePassword(current, newPw); showToast('โœ… Password updated', 'success'); document.getElementById('profileCurrentPw').value = ''; document.getElementById('profileNewPw').value = ''; document.getElementById('profileChangePwForm').style.display = 'none'; } catch (e) { showToast('โŒ ' + e.message, 'error'); } } // โ”€โ”€ API Providers (managed mode) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function updateProviderSection() { const managed = document.getElementById('managedProviders'); if (Backend.isManaged) { managed.style.display = ''; loadProviderList(); } else { managed.style.display = 'none'; } } async function loadProviderList() { const container = document.getElementById('providerList'); container.innerHTML = '
Loading...
'; try { const data = await Backend.listConfigs(); const configs = data.configs || data.data || data || []; const list = Array.isArray(configs) ? configs : []; if (list.length === 0) { container.innerHTML = '
No providers configured. Add one to start chatting.
'; return; } container.innerHTML = list.map(c => `
${escapeHtmlUI(c.name)} ${escapeHtmlUI(c.provider)} ยท ${escapeHtmlUI(c.model_default || 'no default')}
${c.has_key ? '๐Ÿ”‘' : 'โš ๏ธ'} ${c.user_id ? `` : 'global'}
`).join(''); } catch (e) { container.innerHTML = '
Failed to load providers: ' + escapeHtmlUI(e.message) + '
'; } } function showProviderForm() { document.getElementById('providerAddForm').style.display = ''; document.getElementById('providerShowAddBtn').style.display = 'none'; // Set default endpoint based on provider type updateProviderEndpointHint(); } function hideProviderForm() { document.getElementById('providerAddForm').style.display = 'none'; document.getElementById('providerShowAddBtn').style.display = ''; clearProviderForm(); } function clearProviderForm() { document.getElementById('providerName').value = ''; document.getElementById('providerType').value = 'openai'; document.getElementById('providerEndpoint').value = ''; document.getElementById('providerApiKey').value = ''; document.getElementById('providerDefaultModel').value = ''; } function updateProviderEndpointHint() { const type = document.getElementById('providerType').value; const endpoint = document.getElementById('providerEndpoint'); if (!endpoint.value) { endpoint.placeholder = type === 'anthropic' ? 'https://api.anthropic.com' : 'https://api.openai.com/v1'; } } async function handleCreateProvider() { const name = document.getElementById('providerName').value.trim(); const provider = document.getElementById('providerType').value; const endpoint = document.getElementById('providerEndpoint').value.trim(); const apiKey = document.getElementById('providerApiKey').value.trim(); const modelDefault = document.getElementById('providerDefaultModel').value.trim(); if (!name || !endpoint || !apiKey) { showToast('โš ๏ธ Name, endpoint, and API key are required', 'warning'); return; } try { await Backend.createConfig(name, provider, endpoint, apiKey, modelDefault || null); showToast('โœ… Provider added', 'success'); hideProviderForm(); loadProviderList(); fetchModels(false); // Refresh model list } catch (e) { showToast('โŒ ' + e.message, 'error'); } } async function deleteProvider(configId, name) { if (!confirm('Remove provider "' + name + '"?')) return; try { await Backend.deleteConfig(configId); showToast('โœ… Provider removed', 'success'); loadProviderList(); fetchModels(false); } catch (e) { showToast('โŒ ' + e.message, 'error'); } } function escapeHtmlUI(str) { const div = document.createElement('div'); div.textContent = str || ''; return div.innerHTML; } function closeSettings() { document.getElementById('settingsModal').classList.remove('active'); } function toggleSidebar() { document.getElementById('sidebar').classList.toggle('collapsed'); } function renderChatHistory() { const container = document.getElementById('chatHistory'); if (State.chats.length === 0) { container.innerHTML = '
No chat history
'; return; } container.innerHTML = State.chats.map(chat => `
${escapeHtml(chat.title)}
`).join(''); } function renderMessages(messages) { const container = document.getElementById('chatMessages'); if (!messages || messages.length === 0) { newChat(); return; } container.innerHTML = messages .filter(m => m.role !== 'system') .map((msg, idx) => createMessageHTML(msg, idx)) .join(''); scrollToBottom(); } function createMessageHTML(message, index) { const isUser = message.role === 'user'; const time = message.timestamp ? new Date(message.timestamp).toLocaleTimeString() : ''; const formattedContent = formatMessage(message.content); return `
${isUser ? '๐Ÿ‘ค' : '๐Ÿค–'}
${isUser ? 'You' : 'Assistant'}
${time ? `
${time}
` : ''}
${formattedContent}
`; } function formatMessage(content) { let formatted = content; const thinkingBlocks = []; // Extract thinking blocks FIRST (before escaping) if (State.settings.showThinking) { formatted = formatted.replace(/([\s\S]*?)<\/thinking>/gi, (match, thinkingContent) => { const blockId = 'thinking-' + Math.random().toString(36).substr(2, 9); // Store raw content; we'll escape it when reinserting thinkingBlocks.push({ id: blockId, content: thinkingContent.trim() }); return `__THINKPLACEHOLDER_${blockId}__`; }); } else { formatted = formatted.replace(/[\s\S]*?<\/thinking>/gi, ''); } // Escape HTML for the entire string (placeholders are plain text, safe to escape) formatted = escapeHtml(formatted); // Restore thinking blocks with properly-escaped content (single escape only) for (const block of thinkingBlocks) { const escapedContent = escapeHtml(block.content).replace(/\n/g, '
'); formatted = formatted.replace(`__THINKPLACEHOLDER_${block.id}__`, `
โ–ผ ๐Ÿ’ญ Thinking
${escapedContent}
`); } // Code blocks with copy button formatted = formatted.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, lang, code) => { const codeId = 'code-' + Math.random().toString(36).substr(2, 9); return `
${code.trim()}
`; }); // Inline code formatted = formatted.replace(/`([^`]+)`/g, '$1'); // Bold formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '$1'); // Italic formatted = formatted.replace(/\*([^*]+)\*/g, '$1'); // Line breaks (but not inside pre/code) formatted = formatted.replace(/\n/g, '
'); return formatted; } function toggleThinkingBlock(blockId) { const block = document.getElementById(blockId); if (block) { block.classList.toggle('collapsed'); } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function scrollToBottom() { const container = document.getElementById('chatMessages'); container.scrollTop = container.scrollHeight; } function updateRegenerateButton(messages) { const btn = document.getElementById('regenerateBtn'); const hasAssistantMessage = messages && messages.some(m => m.role === 'assistant'); btn.style.display = hasAssistantMessage ? 'flex' : 'none'; } function showToast(message, type = 'success') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = ` ${message} `; container.appendChild(toast); setTimeout(() => toast.remove(), 5000); } // Message actions function copyMessage(index) { const chat = getCurrentChat(); if (chat && chat.messages[index]) { navigator.clipboard.writeText(chat.messages[index].content) .then(() => showToast('๐Ÿ“‹ Copied to clipboard', 'success')) .catch(() => showToast('โŒ Failed to copy', 'error')); } } function copyCode(codeId) { const codeElement = document.getElementById(codeId); if (codeElement) { navigator.clipboard.writeText(codeElement.textContent) .then(() => showToast('๐Ÿ“‹ Code copied', 'success')) .catch(() => showToast('โŒ Failed to copy', 'error')); } } function exportChat(format) { const chat = getCurrentChat(); if (!chat) { showToast('โš ๏ธ No chat to export', 'warning'); return; } let content, filename, mimeType; const title = chat.title.replace(/[^a-z0-9]/gi, '_'); switch (format) { case 'markdown': content = `# ${chat.title}\n\n` + chat.messages .filter(m => m.role !== 'system') .map(m => `## ${m.role === 'user' ? 'You' : 'Assistant'}\n\n${m.content}`) .join('\n\n---\n\n'); filename = `${title}.md`; mimeType = 'text/markdown'; break; case 'json': content = JSON.stringify(chat, null, 2); filename = `${title}.json`; mimeType = 'application/json'; break; case 'text': content = chat.messages .filter(m => m.role !== 'system') .map(m => `[${m.role === 'user' ? 'You' : 'Assistant'}]\n${m.content}`) .join('\n\n'); filename = `${title}.txt`; mimeType = 'text/plain'; break; } const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); showToast(`๐Ÿ“ฅ Exported as ${format}`, 'success'); document.getElementById('exportDropdown').classList.remove('show'); }