// ========================================== // 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(); document.getElementById('settingsModal').classList.add('active'); } 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; // Extract and format thinking blocks FIRST (before escaping) if (State.settings.showThinking) { formatted = formatted.replace(/([\s\S]*?)<\/thinking>/gi, (match, thinkingContent) => { const escapedContent = escapeHtml(thinkingContent.trim()); const blockId = 'thinking-' + Math.random().toString(36).substr(2, 9); return `__THINKING_BLOCK_${blockId}__${escapedContent}__END_THINKING_BLOCK__`; }); } else { // Remove thinking blocks if setting is off formatted = formatted.replace(/[\s\S]*?<\/thinking>/gi, ''); } // Now escape HTML for the rest formatted = escapeHtml(formatted); // Restore thinking blocks with proper HTML formatted = formatted.replace(/__THINKING_BLOCK_([\w-]+)__([\s\S]*?)__END_THINKING_BLOCK__/g, (match, blockId, content) => { return `
💭 Thinking
${content.replace(/\n/g, '
')}
`; }); // 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'); }