// SPDX-FileCopyrightText: Hadad // SPDX-License-Identifier: Apache-2.0 // Prism for code highlighting Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/'; // UI elements const uiElements = { chatArea: document.getElementById('chatArea'), chatBox: document.getElementById('chatBox'), initialContent: document.getElementById('initialContent'), form: document.getElementById('footerForm'), input: document.getElementById('userInput'), sendBtn: document.getElementById('sendBtn'), stopBtn: document.getElementById('stopBtn'), fileBtn: document.getElementById('fileBtn'), audioBtn: document.getElementById('audioBtn'), fileInput: document.getElementById('fileInput'), audioInput: document.getElementById('audioInput'), filePreview: document.getElementById('filePreview'), audioPreview: document.getElementById('audioPreview'), promptItems: document.querySelectorAll('.prompt-item'), chatHeader: document.getElementById('chatHeader'), clearBtn: document.getElementById('clearBtn'), messageLimitWarning: document.getElementById('messageLimitWarning'), conversationTitle: document.getElementById('conversationTitle'), sidebar: document.getElementById('sidebar'), sidebarToggle: document.getElementById('sidebarToggle'), conversationList: document.getElementById('conversationList'), newConversationBtn: document.getElementById('newConversationBtn'), swipeHint: document.getElementById('swipeHint'), settingsBtn: document.getElementById('settingsBtn'), settingsModal: document.getElementById('settingsModal'), closeSettingsBtn: document.getElementById('closeSettingsBtn'), settingsForm: document.getElementById('settingsForm'), historyToggle: document.getElementById('historyToggle') }; // Track state let streamMsg = null; let conversationHistory = JSON.parse(sessionStorage.getItem('conversationHistory') || '[]'); let currentAssistantText = ''; let isRequestActive = false; let abortController = null; let mediaRecorder = null; let audioChunks = []; let isRecording = false; let currentConversationId = window.conversationId || null; let currentConversationTitle = window.conversationTitle || null; let isSidebarOpen = false; // Auto-resize textarea function autoResizeTextarea() { if (uiElements.input) { uiElements.input.style.height = 'auto'; uiElements.input.style.height = `${Math.min(uiElements.input.scrollHeight, 200)}px`; } } // Detect Arabic text function isArabicText(text) { return /[\u0600-\u06FF]/.test(text); } // Initialize page document.addEventListener('DOMContentLoaded', async () => { AOS.init({ duration: 800, easing: 'ease-out-cubic', once: true, offset: 50, }); if (currentConversationId && checkAuth()) { await loadConversation(currentConversationId); } else if (conversationHistory.length > 0) { enterChatView(); conversationHistory.forEach(msg => addMsg(msg.role, msg.content)); } if (checkAuth()) { await loadConversations(); } autoResizeTextarea(); updateSendButtonState(); if (uiElements.swipeHint) { setTimeout(() => { uiElements.swipeHint.style.display = 'none'; }, 3000); } setupTouchGestures(); }); // Check authentication token function checkAuth() { return localStorage.getItem('token'); } // Update send button state function updateSendButtonState() { if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) { uiElements.sendBtn.disabled = uiElements.input.value.trim() === '' && uiElements.fileInput.files.length === 0 && uiElements.audioInput.files.length === 0; } } // Render markdown content with RTL support function renderMarkdown(el) { const raw = el.dataset.text || ''; const isArabic = isArabicText(raw); const html = marked.parse(raw, { gfm: true, breaks: true, smartLists: true, smartypants: false, headerIds: false, }); el.innerHTML = `
${html}
`; const wrapper = el.querySelector('.md-content'); wrapper.querySelectorAll('table').forEach(t => { if (!t.parentNode.classList?.contains('table-wrapper')) { const div = document.createElement('div'); div.className = 'table-wrapper'; t.parentNode.insertBefore(div, t); div.appendChild(t); } }); wrapper.querySelectorAll('hr').forEach(h => h.classList.add('styled-hr')); Prism.highlightAllUnder(wrapper); } // Toggle chat view function enterChatView() { if (uiElements.chatHeader) { uiElements.chatHeader.classList.remove('hidden'); uiElements.chatHeader.setAttribute('aria-hidden', 'false'); if (currentConversationTitle && uiElements.conversationTitle) { uiElements.conversationTitle.textContent = currentConversationTitle; } } if (uiElements.chatBox) uiElements.chatBox.classList.remove('hidden'); if (uiElements.initialContent) uiElements.initialContent.classList.add('hidden'); } // Toggle home view function leaveChatView() { if (uiElements.chatHeader) { uiElements.chatHeader.classList.add('hidden'); uiElements.chatHeader.setAttribute('aria-hidden', 'true'); } if (uiElements.chatBox) uiElements.chatBox.classList.add('hidden'); if (uiElements.initialContent) uiElements.initialContent.classList.remove('hidden'); } // Add chat bubble function addMsg(who, text) { const div = document.createElement('div'); div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`; div.dataset.text = text; renderMarkdown(div); if (uiElements.chatBox) { uiElements.chatBox.appendChild(div); uiElements.chatBox.classList.remove('hidden'); uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; } return div; } // Clear all messages function clearAllMessages() { stopStream(true); conversationHistory = []; sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); currentAssistantText = ''; if (streamMsg) { streamMsg.querySelector('.loading')?.remove(); streamMsg = null; } if (uiElements.chatBox) uiElements.chatBox.innerHTML = ''; if (uiElements.input) uiElements.input.value = ''; if (uiElements.sendBtn) uiElements.sendBtn.disabled = true; if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex'; if (uiElements.filePreview) uiElements.filePreview.style.display = 'none'; if (uiElements.audioPreview) uiElements.audioPreview.style.display = 'none'; if (uiElements.messageLimitWarning) uiElements.messageLimitWarning.classList.add('hidden'); currentConversationId = null; currentConversationTitle = null; if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = 'MGZon AI Assistant'; enterChatView(); autoResizeTextarea(); } // File preview function previewFile() { if (uiElements.fileInput?.files.length > 0) { const file = uiElements.fileInput.files[0]; if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = e => { if (uiElements.filePreview) { uiElements.filePreview.innerHTML = ``; uiElements.filePreview.style.display = 'block'; } if (uiElements.audioPreview) uiElements.audioPreview.style.display = 'none'; updateSendButtonState(); }; reader.readAsDataURL(file); } } if (uiElements.audioInput?.files.length > 0) { const file = uiElements.audioInput.files[0]; if (file.type.startsWith('audio/')) { const reader = new FileReader(); reader.onload = e => { if (uiElements.audioPreview) { uiElements.audioPreview.innerHTML = ``; uiElements.audioPreview.style.display = 'block'; } if (uiElements.filePreview) uiElements.filePreview.style.display = 'none'; updateSendButtonState(); }; reader.readAsDataURL(file); } } } // Voice recording function startVoiceRecording() { if (isRequestActive || isRecording) return; isRecording = true; if (uiElements.sendBtn) uiElements.sendBtn.classList.add('recording'); navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { mediaRecorder = new MediaRecorder(stream); audioChunks = []; mediaRecorder.start(); mediaRecorder.addEventListener('dataavailable', event => audioChunks.push(event.data)); }).catch(err => { console.error('Error accessing microphone:', err); alert('Failed to access microphone. Please check permissions.'); isRecording = false; if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording'); }); } function stopVoiceRecording() { if (mediaRecorder?.state === 'recording') { mediaRecorder.stop(); if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording'); isRecording = false; mediaRecorder.addEventListener('stop', async () => { const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); const formData = new FormData(); formData.append('file', audioBlob, 'voice-message.webm'); await submitAudioMessage(formData); }); } } // Send audio message async function submitAudioMessage(formData) { enterChatView(); addMsg('user', 'Voice message'); conversationHistory.push({ role: 'user', content: 'Voice message' }); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); streamMsg = addMsg('assistant', ''); const loadingEl = document.createElement('span'); loadingEl.className = 'loading'; streamMsg.appendChild(loadingEl); updateUIForRequest(); isRequestActive = true; abortController = new AbortController(); try { const response = await sendRequest('/api/audio-transcription', formData); if (!response.ok) { throw new Error(`Request failed with status ${response.status}`); } const data = await response.json(); if (!data.transcription) throw new Error('No transcription received from server'); const transcription = data.transcription || 'Error: No transcription generated.'; if (streamMsg) { streamMsg.dataset.text = transcription; renderMarkdown(streamMsg); streamMsg.dataset.done = '1'; } conversationHistory.push({ role: 'assistant', content: transcription }); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); if (checkAuth() && currentConversationId) { await saveMessageToConversation(currentConversationId, 'assistant', transcription); } if (checkAuth() && data.conversation_id) { currentConversationId = data.conversation_id; currentConversationTitle = data.conversation_title || 'Untitled Conversation'; if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; history.pushState(null, '', `/chat/${currentConversationId}`); await loadConversations(); } finalizeRequest(); } catch (error) { handleRequestError(error); } } // Helper to send API requests async function sendRequest(endpoint, body, headers = {}) { const token = checkAuth(); if (token) headers['Authorization'] = `Bearer ${token}`; try { const response = await fetch(endpoint, { method: 'POST', body, headers, signal: abortController?.signal, }); if (!response.ok) { if (response.status === 403) { if (uiElements.messageLimitWarning) uiElements.messageLimitWarning.classList.remove('hidden'); throw new Error('Message limit reached. Please log in to continue.'); } if (response.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; throw new Error('Unauthorized. Please log in again.'); } if (response.status === 503) { throw new Error('Model not available. Please try another model.'); } throw new Error(`Request failed with status ${response.status}`); } return response; } catch (error) { if (error.name === 'AbortError') { throw new Error('Request was aborted'); } throw error; } } // Helper to update UI during request function updateUIForRequest() { if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'inline-flex'; if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'none'; if (uiElements.input) uiElements.input.value = ''; if (uiElements.sendBtn) uiElements.sendBtn.disabled = true; if (uiElements.filePreview) uiElements.filePreview.style.display = 'none'; if (uiElements.audioPreview) uiElements.audioPreview.style.display = 'none'; autoResizeTextarea(); } // Helper to finalize request function finalizeRequest() { streamMsg = null; isRequestActive = false; abortController = null; if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex'; if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; } // Helper to handle request errors function handleRequestError(error) { if (streamMsg) { streamMsg.querySelector('.loading')?.remove(); streamMsg.dataset.text = `Error: ${error.message || 'An error occurred during the request.'}`; renderMarkdown(streamMsg); streamMsg.dataset.done = '1'; streamMsg = null; } console.error('Request error:', error); alert(`Error: ${error.message || 'An error occurred during the request.'}`); isRequestActive = false; abortController = null; if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex'; if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; } // Load conversations for sidebar async function loadConversations() { if (!checkAuth()) return; try { const response = await fetch('/api/conversations', { headers: { 'Authorization': `Bearer ${checkAuth()}` } }); if (!response.ok) throw new Error('Failed to load conversations'); const conversations = await response.json(); if (uiElements.conversationList) { uiElements.conversationList.innerHTML = ''; conversations.forEach(conv => { const li = document.createElement('li'); li.className = `flex items-center justify-between text-white hover:bg-gray-700 p-2 rounded cursor-pointer transition-colors ${conv.conversation_id === currentConversationId ? 'bg-gray-700' : ''}`; li.dataset.conversationId = conv.conversation_id; li.innerHTML = `
${conv.title || 'Untitled Conversation'}
`; li.querySelector('[data-conversation-id]').addEventListener('click', () => loadConversation(conv.conversation_id)); li.querySelector('.delete-conversation-btn').addEventListener('click', () => deleteConversation(conv.conversation_id)); uiElements.conversationList.appendChild(li); }); } } catch (error) { console.error('Error loading conversations:', error); alert('Failed to load conversations. Please try again.'); } } // Load conversation from API async function loadConversation(conversationId) { try { const response = await fetch(`/api/conversations/${conversationId}`, { headers: { 'Authorization': `Bearer ${checkAuth()}` } }); if (!response.ok) { if (response.status === 401) window.location.href = '/login'; throw new Error('Failed to load conversation'); } const data = await response.json(); currentConversationId = data.conversation_id; currentConversationTitle = data.title || 'Untitled Conversation'; conversationHistory = data.messages.map(msg => ({ role: msg.role, content: msg.content })); if (uiElements.chatBox) uiElements.chatBox.innerHTML = ''; conversationHistory.forEach(msg => addMsg(msg.role, msg.content)); enterChatView(); if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; history.pushState(null, '', `/chat/${currentConversationId}`); toggleSidebar(false); } catch (error) { console.error('Error loading conversation:', error); alert('Failed to load conversation. Please try again or log in.'); } } // Delete conversation async function deleteConversation(conversationId) { if (!confirm('Are you sure you want to delete this conversation?')) return; try { const response = await fetch(`/api/conversations/${conversationId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${checkAuth()}` } }); if (!response.ok) { if (response.status === 401) window.location.href = '/login'; throw new Error('Failed to delete conversation'); } if (conversationId === currentConversationId) { clearAllMessages(); currentConversationId = null; currentConversationTitle = null; history.pushState(null, '', '/chat'); } await loadConversations(); } catch (error) { console.error('Error deleting conversation:', error); alert('Failed to delete conversation. Please try again.'); } } // Save message to conversation async function saveMessageToConversation(conversationId, role, content) { try { const response = await fetch(`/api/conversations/${conversationId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${checkAuth()}` }, body: JSON.stringify({ role, content }) }); if (!response.ok) throw new Error('Failed to save message'); } catch (error) { console.error('Error saving message:', error); } } // Create new conversation async function createNewConversation() { if (!checkAuth()) { alert('Please log in to create a new conversation.'); window.location.href = '/login'; return; } try { const response = await fetch('/api/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${checkAuth()}` }, body: JSON.stringify({ title: 'New Conversation' }) }); if (!response.ok) { if (response.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } throw new Error('Failed to create conversation'); } const data = await response.json(); currentConversationId = data.conversation_id; currentConversationTitle = data.title; conversationHistory = []; sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); if (uiElements.chatBox) uiElements.chatBox.innerHTML = ''; if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; history.pushState(null, '', `/chat/${currentConversationId}`); enterChatView(); await loadConversations(); toggleSidebar(false); } catch (error) { console.error('Error creating conversation:', error); alert('Failed to create new conversation. Please try again.'); } } // Update conversation title async function updateConversationTitle(conversationId, newTitle) { try { const response = await fetch(`/api/conversations/${conversationId}/title`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${checkAuth()}` }, body: JSON.stringify({ title: newTitle }) }); if (!response.ok) throw new Error('Failed to update title'); const data = await response.json(); currentConversationTitle = data.title; if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; await loadConversations(); } catch (error) { console.error('Error updating title:', error); alert('Failed to update conversation title.'); } } // Toggle sidebar function toggleSidebar(show) { if (uiElements.sidebar) { if (window.innerWidth >= 768) { isSidebarOpen = true; uiElements.sidebar.style.transform = 'translateX(0)'; if (uiElements.swipeHint) uiElements.swipeHint.style.display = 'none'; } else { isSidebarOpen = show !== undefined ? show : !isSidebarOpen; uiElements.sidebar.style.transform = isSidebarOpen ? 'translateX(0)' : 'translateX(-100%)'; if (uiElements.swipeHint && !isSidebarOpen) { uiElements.swipeHint.style.display = 'block'; setTimeout(() => { uiElements.swipeHint.style.display = 'none'; }, 3000); } else if (uiElements.swipeHint) { uiElements.swipeHint.style.display = 'none'; } } } } // Setup touch gestures with Hammer.js function setupTouchGestures() { if (!uiElements.sidebar) return; const hammer = new Hammer(uiElements.sidebar); const mainContent = document.querySelector('.flex-1'); const hammerMain = new Hammer(mainContent); hammer.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL }); hammer.on('pan', e => { if (!isSidebarOpen) return; let translateX = Math.max(-uiElements.sidebar.offsetWidth, Math.min(0, e.deltaX)); uiElements.sidebar.style.transform = `translateX(${translateX}px)`; }); hammer.on('panend', e => { if (!isSidebarOpen) return; if (e.deltaX < -50) { toggleSidebar(false); } else { toggleSidebar(true); } }); hammerMain.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL }); hammerMain.on('panstart', e => { if (isSidebarOpen) return; if (e.center.x < 50 || e.center.x > window.innerWidth - 50) { uiElements.sidebar.style.transition = 'none'; } }); hammerMain.on('pan', e => { if (isSidebarOpen) return; if (e.center.x < 50 || e.center.x > window.innerWidth - 50) { let translateX = e.center.x < 50 ? Math.min(uiElements.sidebar.offsetWidth, Math.max(0, e.deltaX)) : Math.max(-uiElements.sidebar.offsetWidth, Math.min(0, e.deltaX)); uiElements.sidebar.style.transform = `translateX(${translateX - uiElements.sidebar.offsetWidth}px)`; } }); hammerMain.on('panend', e => { uiElements.sidebar.style.transition = 'transform 0.3s ease-in-out'; if (e.center.x < 50 && e.deltaX > 50) { toggleSidebar(true); } else if (e.center.x > window.innerWidth - 50 && e.deltaX < -50) { toggleSidebar(true); } else { toggleSidebar(false); } }); } // Send user message async function submitMessage() { if (isRequestActive || isRecording) return; let message = uiElements.input?.value.trim() || ''; let payload = null; let formData = null; let endpoint = '/api/chat'; let headers = {}; let inputType = 'text'; let outputFormat = 'text'; let title = null; if (uiElements.fileInput?.files.length > 0) { const file = uiElements.fileInput.files[0]; if (file.type.startsWith('image/')) { endpoint = '/api/image-analysis'; inputType = 'image'; message = 'Analyze this image'; formData = new FormData(); formData.append('file', file); formData.append('output_format', 'text'); } } else if (uiElements.audioInput?.files.length > 0) { const file = uiElements.audioInput.files[0]; if (file.type.startsWith('audio/')) { endpoint = '/api/audio-transcription'; inputType = 'audio'; message = 'Transcribe this audio'; formData = new FormData(); formData.append('file', file); } } else if (message) { payload = { message, system_prompt: isArabicText(message) ? 'أنت مساعد ذكي تقدم إجابات مفصلة ومنظمة باللغة العربية، مع ضمان الدقة والوضوح.' : 'You are an expert assistant providing detailed, comprehensive, and well-structured responses.', history: conversationHistory, temperature: 0.7, max_new_tokens: 128000, enable_browsing: true, output_format: 'text', title: title }; headers['Content-Type'] = 'application/json'; } else { return; } enterChatView(); addMsg('user', message); conversationHistory.push({ role: 'user', content: message }); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); streamMsg = addMsg('assistant', ''); const loadingEl = document.createElement('span'); loadingEl.className = 'loading'; streamMsg.appendChild(loadingEl); updateUIForRequest(); isRequestActive = true; abortController = new AbortController(); try { const response = await sendRequest(endpoint, payload ? JSON.stringify(payload) : formData, headers); if (endpoint === '/api/audio-transcription') { const data = await response.json(); if (!data.transcription) throw new Error('No transcription received from server'); const transcription = data.transcription || 'Error: No transcription generated.'; if (streamMsg) { streamMsg.dataset.text = transcription; renderMarkdown(streamMsg); streamMsg.dataset.done = '1'; } conversationHistory.push({ role: 'assistant', content: transcription }); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); if (checkAuth() && currentConversationId) { await saveMessageToConversation(currentConversationId, 'assistant', transcription); } if (checkAuth() && data.conversation_id) { currentConversationId = data.conversation_id; currentConversationTitle = data.conversation_title || 'Untitled Conversation'; if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; history.pushState(null, '', `/chat/${currentConversationId}`); await loadConversations(); } } else if (endpoint === '/api/image-analysis') { const data = await response.json(); const analysis = data.image_analysis || 'Error: No analysis generated.'; if (streamMsg) { streamMsg.dataset.text = analysis; renderMarkdown(streamMsg); streamMsg.dataset.done = '1'; } conversationHistory.push({ role: 'assistant', content: analysis }); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); if (checkAuth() && currentConversationId) { await saveMessageToConversation(currentConversationId, 'assistant', analysis); } if (checkAuth() && data.conversation_id) { currentConversationId = data.conversation_id; currentConversationTitle = data.conversation_title || 'Untitled Conversation'; if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; history.pushState(null, '', `/chat/${currentConversationId}`); await loadConversations(); } } else { const contentType = response.headers.get('Content-Type'); if (contentType?.includes('application/json')) { const data = await response.json(); const responseText = data.response || 'Error: No response generated.'; if (streamMsg) { streamMsg.dataset.text = responseText; renderMarkdown(streamMsg); streamMsg.dataset.done = '1'; } conversationHistory.push({ role: 'assistant', content: responseText }); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); if (checkAuth() && currentConversationId) { await saveMessageToConversation(currentConversationId, 'assistant', responseText); } if (checkAuth() && data.conversation_id) { currentConversationId = data.conversation_id; currentConversationTitle = data.conversation_title || 'Untitled Conversation'; if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; history.pushState(null, '', `/chat/${currentConversationId}`); await loadConversations(); } } else { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) { if (!buffer.trim()) throw new Error('Empty response from server'); break; } buffer += decoder.decode(value, { stream: true }); if (streamMsg) { streamMsg.dataset.text = buffer; currentAssistantText = buffer; streamMsg.querySelector('.loading')?.remove(); renderMarkdown(streamMsg); if (uiElements.chatBox) uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; } } if (streamMsg) streamMsg.dataset.done = '1'; conversationHistory.push({ role: 'assistant', content: buffer }); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); if (checkAuth() && currentConversationId) { await saveMessageToConversation(currentConversationId, 'assistant', buffer); } } } finalizeRequest(); } catch (error) { handleRequestError(error); } } // Stop streaming function stopStream(forceCancel = false) { if (!isRequestActive && !isRecording) return; if (isRecording) stopVoiceRecording(); isRequestActive = false; if (abortController) { abortController.abort(); abortController = null; } if (streamMsg && !forceCancel) { streamMsg.querySelector('.loading')?.remove(); streamMsg.dataset.text += ''; renderMarkdown(streamMsg); streamMsg.dataset.done = '1'; streamMsg = null; } if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex'; if (uiElements.stopBtn) uiElements.stopBtn.style.pointerEvents = 'auto'; } // Settings Modal if (uiElements.settingsBtn) { uiElements.settingsBtn.addEventListener('click', () => { if (!checkAuth()) { alert('Please log in to access settings.'); window.location.href = '/login'; return; } uiElements.settingsModal.classList.remove('hidden'); fetch('/api/settings', { headers: { 'Authorization': `Bearer ${checkAuth()}` } }) .then(res => { if (!res.ok) { if (res.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } throw new Error('Failed to fetch settings'); } return res.json(); }) .then(data => { document.getElementById('display_name').value = data.user_settings.display_name || ''; document.getElementById('preferred_model').value = data.user_settings.preferred_model || 'standard'; document.getElementById('job_title').value = data.user_settings.job_title || ''; document.getElementById('education').value = data.user_settings.education || ''; document.getElementById('interests').value = data.user_settings.interests || ''; document.getElementById('additional_info').value = data.user_settings.additional_info || ''; document.getElementById('conversation_style').value = data.user_settings.conversation_style || 'default'; const modelSelect = document.getElementById('preferred_model'); modelSelect.innerHTML = ''; data.available_models.forEach(model => { const option = document.createElement('option'); option.value = model.alias; option.textContent = `${model.alias} - ${model.description}`; modelSelect.appendChild(option); }); const styleSelect = document.getElementById('conversation_style'); styleSelect.innerHTML = ''; data.conversation_styles.forEach(style => { const option = document.createElement('option'); option.value = style; option.textContent = style.charAt(0).toUpperCase() + style.slice(1); styleSelect.appendChild(option); }); }) .catch(err => { console.error('Error fetching settings:', err); alert('Failed to load settings. Please try again.'); }); }); } if (uiElements.closeSettingsBtn) { uiElements.closeSettingsBtn.addEventListener('click', () => { uiElements.settingsModal.classList.add('hidden'); }); } if (uiElements.settingsForm) { uiElements.settingsForm.addEventListener('submit', (e) => { e.preventDefault(); if (!checkAuth()) { alert('Please log in to save settings.'); window.location.href = '/login'; return; } const formData = new FormData(uiElements.settingsForm); const data = Object.fromEntries(formData); fetch('/users/me', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${checkAuth()}` }, body: JSON.stringify(data) }) .then(res => { if (!res.ok) { if (res.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } throw new Error('Failed to update settings'); } return res.json(); }) .then(() => { alert('Settings updated successfully!'); uiElements.settingsModal.classList.add('hidden'); toggleSidebar(false); }) .catch(err => { console.error('Error updating settings:', err); alert('Error updating settings: ' + err.message); }); }); } // History Toggle if (uiElements.historyToggle) { uiElements.historyToggle.addEventListener('click', () => { if (uiElements.conversationList) { uiElements.conversationList.classList.toggle('hidden'); uiElements.historyToggle.innerHTML = uiElements.conversationList.classList.contains('hidden') ? ` Show History` : ` Hide History`; } }); } // Event listeners uiElements.promptItems.forEach(p => { p.addEventListener('click', e => { e.preventDefault(); if (uiElements.input) { uiElements.input.value = p.dataset.prompt; autoResizeTextarea(); } if (uiElements.sendBtn) uiElements.sendBtn.disabled = false; submitMessage(); }); }); if (uiElements.fileBtn) uiElements.fileBtn.addEventListener('click', () => uiElements.fileInput?.click()); if (uiElements.audioBtn) uiElements.audioBtn.addEventListener('click', () => uiElements.audioInput?.click()); if (uiElements.fileInput) uiElements.fileInput.addEventListener('change', previewFile); if (uiElements.audioInput) uiElements.audioInput.addEventListener('change', previewFile); if (uiElements.sendBtn) { uiElements.sendBtn.addEventListener('mousedown', e => { if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return; startVoiceRecording(); }); uiElements.sendBtn.addEventListener('mouseup', () => isRecording && stopVoiceRecording()); uiElements.sendBtn.addEventListener('mouseleave', () => isRecording && stopVoiceRecording()); uiElements.sendBtn.addEventListener('touchstart', e => { e.preventDefault(); if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return; startVoiceRecording(); }); uiElements.sendBtn.addEventListener('touchend', e => { e.preventDefault(); if (isRecording) stopVoiceRecording(); }); uiElements.sendBtn.addEventListener('touchcancel', e => { e.preventDefault(); if (isRecording) stopVoiceRecording(); }); } if (uiElements.form) { uiElements.form.addEventListener('submit', e => { e.preventDefault(); if (!isRecording) submitMessage(); }); } if (uiElements.input) { uiElements.input.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (!isRecording && !uiElements.sendBtn.disabled) submitMessage(); } }); uiElements.input.addEventListener('input', () => { autoResizeTextarea(); updateSendButtonState(); }); } if (uiElements.stopBtn) { uiElements.stopBtn.addEventListener('click', () => { uiElements.stopBtn.style.pointerEvents = 'none'; stopStream(); }); } if (uiElements.clearBtn) uiElements.clearBtn.addEventListener('click', clearAllMessages); if (uiElements.conversationTitle) { uiElements.conversationTitle.addEventListener('click', () => { if (!checkAuth()) return alert('Please log in to edit the conversation title.'); const newTitle = prompt('Enter new conversation title:', currentConversationTitle || ''); if (newTitle && currentConversationId) { updateConversationTitle(currentConversationId, newTitle); } }); } if (uiElements.sidebarToggle) { uiElements.sidebarToggle.addEventListener('click', () => toggleSidebar()); } if (uiElements.newConversationBtn) { uiElements.newConversationBtn.addEventListener('click', createNewConversation); }