| |
| |
| |
|
|
| console.log('MGZon Chat v5.0 - FINAL FIXED version loaded at:', new Date().toISOString()); |
|
|
| |
| |
| |
|
|
| 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'), |
| cancelSettingsBtn: document.getElementById('cancelSettingsBtn'), |
| settingsForm: document.getElementById('settingsForm'), |
| historyToggle: document.getElementById('historyToggle'), |
| }; |
|
|
| |
| |
| |
|
|
| let conversationHistory = JSON.parse(sessionStorage.getItem('conversationHistory') || '[]'); |
| let currentConversationId = window.conversationId || null; |
| let currentConversationTitle = window.conversationTitle || null; |
| let isRequestActive = false; |
| let isRecording = false; |
| let mediaRecorder = null; |
| let audioChunks = []; |
| let streamMsg = null; |
| let currentAssistantText = ''; |
| let isSidebarOpen = window.innerWidth >= 768; |
| let abortController = null; |
| let attemptCount = 0; |
| let attempts = []; |
|
|
| |
| let lastSentMessage = ''; |
| let lastSentTime = 0; |
|
|
| |
| |
| |
|
|
| function autoResizeTextarea() { |
| if (uiElements.input) { |
| uiElements.input.style.height = 'auto'; |
| uiElements.input.style.height = uiElements.input.scrollHeight + 'px'; |
| updateSendButtonState(); |
| } |
| } |
|
|
| function updateSendButtonState() { |
| if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) { |
| const hasInput = uiElements.input.value.trim() !== '' || |
| uiElements.fileInput.files.length > 0 || |
| uiElements.audioInput.files.length > 0; |
| uiElements.sendBtn.disabled = !hasInput || isRequestActive || isRecording; |
| } |
| } |
|
|
| function detectLanguage(text) { |
| if (!text || typeof text !== 'string') return 'en'; |
| if (/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text)) return 'ar'; |
| if (/[\u0400-\u04FF]/.test(text)) return 'ru'; |
| if (/[\u0370-\u03FF\u1F00-\u1FFF]/.test(text)) return 'el'; |
| if (/[\u0590-\u05FF]/.test(text)) return 'he'; |
| if (/[\u4E00-\u9FFF]/.test(text)) return 'zh'; |
| if (/[\u00C0-\u017F]/.test(text)) { |
| if (text.match(/ç|ã|õ/)) return 'pt'; |
| if (text.match(/ñ|¿|¡/)) return 'es'; |
| if (text.match(/é|è|ê|à|ù|ç/)) return 'fr'; |
| if (text.match(/ß|ä|ö|ü/)) return 'de'; |
| if (text.match(/à|è|ì|ò|ù/)) return 'it'; |
| if (text.match(/á|é|í|ó|ú|ý/)) return 'cs'; |
| if (text.match(/ą|ę|ł|ń|ś|ź|ż/)) return 'pl'; |
| if (text.match(/á|é|í|ó|ú|ő|ű/)) return 'hu'; |
| if (text.match(/ā|ē|ī|ū/)) return 'lv'; |
| if (text.match(/å|ä|ö/)) return 'sv'; |
| if (text.match(/ș|ț/)) return 'ro'; |
| if (text.match(/á|é|í|ó|ú|č|ď|ľ|ň|š|ť|ž/)) return 'sk'; |
| if (text.match(/ç|ğ|ı|ö|ş|ü/)) return 'tr'; |
| if (text.match(/ç|·|l·l/)) return 'ca'; |
| if (text.match(/ij|oe|ui/)) return 'nl'; |
| if (text.match(/ĉ|ĝ|ĥ|ĵ|ŝ|ŭ/)) return 'eo'; |
| if (text.match(/ä|ö/)) return 'fi'; |
| } |
| if (/[a-zA-Z]/.test(text)) { |
| if (text.match(/\b(color|organize|realize)\b/)) return 'en-us'; |
| if (text.match(/\b(colour|organise|realise)\b/)) return 'en-gb'; |
| if (text.match(/\b(whisky|loch)\b/)) return 'en-sc'; |
| return 'en'; |
| } |
| return 'en'; |
| } |
|
|
| function isArabicText(text) { |
| return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text); |
| } |
|
|
| |
| |
| |
|
|
| async function checkAuth() { |
| const urlParams = new URLSearchParams(window.location.search); |
| const accessTokenFromUrl = urlParams.get('access_token'); |
| if (accessTokenFromUrl) { |
| localStorage.setItem('token', accessTokenFromUrl); |
| window.history.replaceState({}, document.title, '/chat'); |
| } |
|
|
| let token = localStorage.getItem('token'); |
| if (!token && typeof Cookies !== 'undefined') { |
| token = Cookies.get('fastapiusersauth'); |
| if (token) localStorage.setItem('token', token); |
| } |
|
|
| if (!token) return { authenticated: false, user: null }; |
|
|
| try { |
| const response = await fetch('/api/verify-token', { |
| method: 'GET', |
| headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/json' } |
| }); |
| const data = await response.json(); |
| if (response.ok && data.status === 'valid') { |
| return { authenticated: true, user: data.user }; |
| } |
| localStorage.removeItem('token'); |
| if (typeof Cookies !== 'undefined') Cookies.remove('fastapiusersauth'); |
| return { authenticated: false, user: null }; |
| } catch (error) { |
| localStorage.removeItem('token'); |
| if (typeof Cookies !== 'undefined') Cookies.remove('fastapiusersauth'); |
| return { authenticated: false, user: null }; |
| } |
| } |
|
|
| async function handleSession() { |
| let sessionId = sessionStorage.getItem('session_id'); |
| if (!sessionId) { |
| sessionId = crypto.randomUUID(); |
| sessionStorage.setItem('session_id', sessionId); |
| } |
| return sessionId; |
| } |
|
|
| |
| |
| |
|
|
| async function updateSidebarAfterLogin() { |
| const authResult = await checkAuth(); |
| const settingsLi = document.querySelector('#settingsBtn')?.closest('li'); |
| const logoutLi = document.querySelector('#logoutBtn')?.closest('li'); |
| const loginLi = document.querySelector('a[href="/login"]')?.closest('li'); |
| const conversationsDiv = document.querySelector('.mt-4'); |
| |
| if (authResult.authenticated) { |
| if (loginLi) loginLi.style.display = 'none'; |
| if (settingsLi) settingsLi.style.display = 'block'; |
| if (logoutLi) logoutLi.style.display = 'block'; |
| if (conversationsDiv) conversationsDiv.style.display = 'block'; |
| } else { |
| if (loginLi) loginLi.style.display = 'block'; |
| if (settingsLi) settingsLi.style.display = 'none'; |
| if (logoutLi) logoutLi.style.display = 'none'; |
| if (conversationsDiv) conversationsDiv.style.display = 'none'; |
| } |
| } |
|
|
| |
| |
| |
|
|
| async function renderMarkdown(el, isStreaming = false) { |
| const raw = el.dataset.text || ''; |
| const lang = detectLanguage(raw); |
| const isRTL = ['ar', 'he'].includes(lang); |
| |
| const html = marked.parse(raw, { |
| gfm: true, |
| breaks: true, |
| smartLists: true, |
| smartypants: false, |
| headerIds: false, |
| }); |
|
|
| const wrapper = document.createElement('div'); |
| wrapper.className = 'md-content ' + (isRTL ? 'rtl' : 'ltr'); |
| wrapper.style.direction = isRTL ? 'rtl' : 'ltr'; |
| wrapper.style.textAlign = isRTL ? 'right' : 'left'; |
| el.innerHTML = ''; |
| el.appendChild(wrapper); |
|
|
| if (isStreaming) { |
| const words = html.split(/(<[^>]+>|[^\s<]+)/); |
| wrapper.innerHTML = ''; |
| for (let i = 0; i < words.length; i++) { |
| const span = document.createElement('span'); |
| span.innerHTML = words[i]; |
| wrapper.appendChild(span); |
| if (!/<[^>]+>/.test(words[i])) { |
| await new Promise(resolve => setTimeout(resolve, 30)); |
| } |
| if (uiElements.chatBox) uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; |
| } |
| } else { |
| wrapper.innerHTML = html; |
| } |
|
|
| wrapper.querySelectorAll('table').forEach(t => { |
| if (!t.parentNode.classList || !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('pre').forEach(pre => { |
| const code = pre.querySelector('code'); |
| if (code) { |
| const copyBtn = document.createElement('button'); |
| copyBtn.className = 'copy-btn'; |
| copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>'; |
| copyBtn.onclick = () => { |
| navigator.clipboard.writeText(code.innerText).then(() => { |
| copyBtn.innerHTML = '<span>Copied!</span>'; |
| setTimeout(() => { |
| copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>'; |
| }, 2000); |
| }); |
| }; |
| pre.appendChild(copyBtn); |
| } |
| }); |
|
|
| Prism.highlightAllUnder(wrapper); |
| if (uiElements.chatBox) { |
| uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; |
| } |
| el.style.display = 'block'; |
| } |
|
|
| |
| |
| |
|
|
| function addMsg(who, text) { |
| const container = document.createElement('div'); |
| container.className = 'message-container'; |
| const div = document.createElement('div'); |
| const lang = detectLanguage(text); |
| const isRTL = ['ar', 'he'].includes(lang); |
| div.className = 'bubble ' + (who === 'user' ? 'bubble-user' : 'bubble-assist') + ' ' + (isRTL ? 'rtl' : 'ltr'); |
| div.dataset.text = text; |
| |
| renderMarkdown(div); |
| div.style.display = 'block'; |
|
|
| const actions = document.createElement('div'); |
| actions.className = 'message-actions'; |
|
|
| const copyBtn = document.createElement('button'); |
| copyBtn.className = 'action-btn'; |
| copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>'; |
| copyBtn.onclick = () => { |
| navigator.clipboard.writeText(text).then(() => { |
| copyBtn.textContent = 'Copied!'; |
| setTimeout(() => { |
| copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>'; |
| }, 2000); |
| }); |
| }; |
| actions.appendChild(copyBtn); |
|
|
| if (who === 'assistant') { |
| const retryBtn = document.createElement('button'); |
| retryBtn.className = 'action-btn'; |
| retryBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>'; |
| retryBtn.onclick = () => submitMessage(); |
| actions.appendChild(retryBtn); |
|
|
| const speakBtn = document.createElement('button'); |
| speakBtn.className = 'action-btn'; |
| speakBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M11 4.702a.705.705 0 00-1.203-.498L6.413 7.587A1.4 1.4 0 015.416 8H3a1 1 0 00-1 1v6a1 1 0 001 1h2.416a1.4 1.4 0 01.997.413l3.383 3.384A.705.705 0 0011 19.298z"></path><path d="M16 9a5 5 0 010 6"></path></svg>'; |
| speakBtn.onclick = () => { |
| window.speechSynthesis.cancel(); |
| const utterance = new SpeechSynthesisUtterance(text); |
| utterance.lang = isArabicText(text) ? 'ar-SA' : 'en-US'; |
| window.speechSynthesis.speak(utterance); |
| }; |
| actions.appendChild(speakBtn); |
|
|
| const stopSpeakBtn = document.createElement('button'); |
| stopSpeakBtn.className = 'action-btn'; |
| stopSpeakBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="6" y="6" width="12" height="12"></rect></svg>'; |
| stopSpeakBtn.onclick = () => window.speechSynthesis.cancel(); |
| actions.appendChild(stopSpeakBtn); |
| } |
|
|
| if (who === 'user') { |
| const editBtn = document.createElement('button'); |
| editBtn.className = 'action-btn'; |
| editBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>'; |
| editBtn.onclick = () => editMessage(div, text, container); |
| actions.appendChild(editBtn); |
| } |
|
|
| container.appendChild(div); |
| container.appendChild(actions); |
| |
| if (uiElements.chatBox) { |
| uiElements.chatBox.appendChild(container); |
| uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; |
| if (conversationHistory.length === 0 && uiElements.initialContent) { |
| uiElements.initialContent.classList.add('hidden'); |
| uiElements.initialContent.style.display = 'none'; |
| } |
| } else { |
| document.body.appendChild(container); |
| } |
|
|
| if (who === 'user') { |
| conversationHistory.push({ role: 'user', content: text }); |
| sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
| } |
|
|
| return div; |
| } |
|
|
| function editMessage(div, originalText, container) { |
| const isRTL = ['ar', 'he'].includes(detectLanguage(originalText)); |
| div.innerHTML = ''; |
|
|
| const textarea = document.createElement('textarea'); |
| textarea.className = 'edit-textarea'; |
| textarea.value = originalText; |
| textarea.style.direction = isRTL ? 'rtl' : 'ltr'; |
| textarea.style.textAlign = isRTL ? 'right' : 'left'; |
| textarea.style.width = '100%'; |
| textarea.style.minHeight = '100px'; |
| textarea.style.padding = '10px'; |
| textarea.style.border = '1px solid #ccc'; |
| textarea.style.borderRadius = '5px'; |
| textarea.style.resize = 'vertical'; |
|
|
| const saveBtn = document.createElement('button'); |
| saveBtn.className = 'action-btn save-btn'; |
| saveBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M17 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>'; |
| saveBtn.title = 'Save Changes'; |
|
|
| const cancelBtn = document.createElement('button'); |
| cancelBtn.className = 'action-btn cancel-btn'; |
| cancelBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/></svg>'; |
| cancelBtn.title = 'Cancel'; |
|
|
| const actionsDiv = document.createElement('div'); |
| actionsDiv.className = 'edit-actions'; |
| actionsDiv.style.display = 'flex'; |
| actionsDiv.style.gap = '10px'; |
| actionsDiv.style.marginTop = '10px'; |
| actionsDiv.appendChild(saveBtn); |
| actionsDiv.appendChild(cancelBtn); |
|
|
| div.appendChild(textarea); |
| div.appendChild(actionsDiv); |
| textarea.focus(); |
|
|
| saveBtn.onclick = async () => { |
| const newText = textarea.value.trim(); |
| if (newText && newText !== originalText) { |
| div.dataset.text = newText; |
| renderMarkdown(div); |
| const index = conversationHistory.findIndex(msg => msg.role === 'user' && msg.content === originalText); |
| if (index !== -1) { |
| conversationHistory[index].content = newText; |
| sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
| if (index + 1 < conversationHistory.length && conversationHistory[index + 1].role === 'assistant') { |
| conversationHistory.splice(index + 1, 1); |
| const nextMessage = container.nextSibling; |
| if (nextMessage) nextMessage.remove(); |
| } |
| uiElements.input.value = newText; |
| await submitMessage(); |
| } |
| } else { |
| div.dataset.text = originalText; |
| renderMarkdown(div); |
| } |
| container.querySelector('.message-actions').style.display = 'flex'; |
| div.querySelector('.edit-actions').remove(); |
| }; |
|
|
| cancelBtn.onclick = () => { |
| div.dataset.text = originalText; |
| renderMarkdown(div); |
| container.querySelector('.message-actions').style.display = 'flex'; |
| div.querySelector('.edit-actions').remove(); |
| }; |
|
|
| container.querySelector('.message-actions').style.display = 'none'; |
| } |
|
|
| |
| |
| |
|
|
| function enterChatView(force = false) { |
| if (uiElements.chatHeader) { |
| uiElements.chatHeader.classList.remove('hidden'); |
| if (currentConversationTitle && uiElements.conversationTitle) { |
| uiElements.conversationTitle.textContent = currentConversationTitle; |
| } |
| } |
| if (uiElements.chatArea) { |
| uiElements.chatArea.classList.remove('hidden'); |
| uiElements.chatArea.style.display = force ? 'flex !important' : 'flex'; |
| } |
| if (uiElements.chatBox) { |
| uiElements.chatBox.classList.remove('hidden'); |
| uiElements.chatBox.style.display = force ? 'flex !important' : 'flex'; |
| } |
| if (uiElements.initialContent && (conversationHistory.length > 0 || currentConversationId)) { |
| uiElements.initialContent.classList.add('hidden'); |
| uiElements.initialContent.style.display = 'none'; |
| } |
| if (uiElements.form) { |
| uiElements.form.classList.remove('hidden'); |
| uiElements.form.style.display = force ? 'flex !important' : 'flex'; |
| } |
| } |
|
|
| function leaveChatView() { |
| if (uiElements.chatHeader) uiElements.chatHeader.classList.add('hidden'); |
| if (uiElements.chatBox) uiElements.chatBox.classList.add('hidden'); |
| if (uiElements.initialContent && conversationHistory.length === 0 && !currentConversationId) { |
| uiElements.initialContent.classList.remove('hidden'); |
| uiElements.initialContent.style.display = 'block'; |
| } |
| if (uiElements.form) uiElements.form.classList.add('hidden'); |
| } |
|
|
| |
| |
| |
|
|
| function clearAllMessages() { |
| stopStream(true); |
| conversationHistory = []; |
| sessionStorage.removeItem('conversationHistory'); |
| currentAssistantText = ''; |
| if (streamMsg) { |
| if (streamMsg.querySelector('.loading')) 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'; |
| if (uiElements.initialContent) { |
| uiElements.initialContent.classList.remove('hidden'); |
| uiElements.initialContent.style.display = 'block'; |
| } |
| leaveChatView(); |
| autoResizeTextarea(); |
| } |
|
|
| |
| |
| |
|
|
| function previewFile() { |
| if (uiElements.fileInput && 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 = '<img src="' + e.target.result + '" class="upload-preview">'; |
| uiElements.filePreview.style.display = 'block'; |
| } |
| if (uiElements.audioPreview) uiElements.audioPreview.style.display = 'none'; |
| updateSendButtonState(); |
| }; |
| reader.readAsDataURL(file); |
| } |
| } |
| if (uiElements.audioInput && 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 = '<audio controls src="' + e.target.result + '"></audio>'; |
| uiElements.audioPreview.style.display = 'block'; |
| } |
| if (uiElements.filePreview) uiElements.filePreview.style.display = 'none'; |
| updateSendButtonState(); |
| }; |
| reader.readAsDataURL(file); |
| } |
| } |
| } |
|
|
| |
| |
| |
|
|
| function startVoiceRecording() { |
| if (isRequestActive || isRecording) return; |
| console.log('Starting voice recording...'); |
| 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 && 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); |
| }); |
| } |
| } |
|
|
| |
| |
| |
|
|
| async function submitAudioMessage(formData) { |
| if (uiElements.initialContent && !uiElements.initialContent.classList.contains('hidden')) { |
| uiElements.initialContent.classList.add('hidden'); |
| uiElements.initialContent.style.display = 'none'; |
| } |
| enterChatView(); |
| addMsg('user', 'Voice message'); |
| const authResult = await checkAuth(); |
| if (!authResult.authenticated) { |
| 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; |
| if (streamMsg) { |
| streamMsg.dataset.text = transcription; |
| renderMarkdown(streamMsg); |
| streamMsg.dataset.done = '1'; |
| } |
| const authResult2 = await checkAuth(); |
| if (!authResult2.authenticated) { |
| conversationHistory.push({ role: 'assistant', content: transcription }); |
| sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
| } |
| if (authResult2.authenticated && 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); |
| } |
| } |
|
|
| |
| |
| |
|
|
| async function sendRequest(endpoint, body, headers = {}) { |
| const token = localStorage.getItem('token'); |
| if (token) headers['Authorization'] = 'Bearer ' + token; |
| headers['X-Session-ID'] = await handleSession(); |
| |
| try { |
| const response = await fetch(endpoint, { |
| method: 'POST', |
| body: body, |
| headers: headers, |
| signal: abortController ? abortController.signal : undefined, |
| }); |
| 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; |
| } |
| } |
|
|
| 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(); |
| } |
|
|
| function finalizeRequest() { |
| streamMsg = null; |
| isRequestActive = false; |
| abortController = null; |
| if (uiElements.sendBtn) { |
| uiElements.sendBtn.style.display = 'inline-flex'; |
| uiElements.sendBtn.disabled = false; |
| } |
| if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; |
| updateSendButtonState(); |
| } |
|
|
| function handleRequestError(error) { |
| if (streamMsg) { |
| if (streamMsg.querySelector('.loading')) streamMsg.querySelector('.loading').remove(); |
| streamMsg.dataset.text = 'Error: ' + (error.message || 'An error occurred during the request.'); |
| const retryBtn = document.createElement('button'); |
| retryBtn.innerText = 'Retry'; |
| retryBtn.className = 'retry-btn text-sm text-blue-400 hover:text-blue-600'; |
| retryBtn.onclick = () => submitMessage(); |
| streamMsg.appendChild(retryBtn); |
| 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; |
| (async () => { |
| const authResult = await checkAuth(); |
| if (!authResult.authenticated) { |
| sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
| } |
| })(); |
| if (uiElements.sendBtn) { |
| uiElements.sendBtn.style.display = 'inline-flex'; |
| uiElements.sendBtn.disabled = false; |
| } |
| if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; |
| enterChatView(); |
| } |
|
|
| |
| |
| |
|
|
| async function loadConversations() { |
| const authResult = await checkAuth(); |
| if (!authResult.authenticated) return; |
| try { |
| const response = await fetch('/api/conversations', { |
| headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') } |
| }); |
| 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'); |
| const isRTL = isArabicText(conv.title); |
| 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 = ` |
| <div class="flex items-center flex-1" style="direction: ${isRTL ? 'rtl' : 'ltr'};" data-conversation-id="${conv.conversation_id}"> |
| <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path> |
| </svg> |
| <span class="truncate flex-1">${conv.title || 'Untitled Conversation'}</span> |
| </div> |
| <button class="delete-conversation-btn text-red-400 hover:text-red-600 p-1" title="Delete Conversation" data-conversation-id="${conv.conversation_id}"> |
| <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5-4h4M3 7h18"></path> |
| </svg> |
| </button> |
| `; |
| 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.'); |
| } |
| } |
|
|
| async function loadConversation(conversationId) { |
| try { |
| const response = await fetch('/api/conversations/' + conversationId, { |
| headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') } |
| }); |
| 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.'); |
| } |
| } |
|
|
| 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 ' + localStorage.getItem('token') } |
| }); |
| 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.'); |
| } |
| } |
|
|
| async function createNewConversation() { |
| const authResult = await checkAuth(); |
| if (!authResult.authenticated) { |
| 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 ' + localStorage.getItem('token') |
| }, |
| 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.removeItem('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.'); |
| } |
| if (uiElements.chatBox) { |
| uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; |
| } |
| } |
|
|
| async function updateConversationTitle(conversationId, newTitle) { |
| try { |
| const response = await fetch('/api/conversations/' + conversationId + '/title', { |
| method: 'PUT', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': 'Bearer ' + localStorage.getItem('token') |
| }, |
| 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.'); |
| } |
| } |
|
|
| |
| |
| |
|
|
| 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'; |
| } |
| } |
| } |
| } |
|
|
| 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)'; |
| uiElements.sidebar.style.transition = 'none'; |
| }); |
| hammer.on('panend', e => { |
| uiElements.sidebar.style.transition = 'transform 0.3s ease-in-out'; |
| 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); |
| }); |
| } |
|
|
| |
| |
| |
|
|
| function addAttemptHistory(who, text) { |
| attemptCount++; |
| attempts.push(text); |
| const container = document.createElement('div'); |
| container.className = 'message-container'; |
| const div = document.createElement('div'); |
| const isRTL = isArabicText(text); |
| div.className = 'bubble ' + (who === 'user' ? 'bubble-user' : 'bubble-assist') + ' ' + (isRTL ? 'rtl' : 'ltr'); |
| div.dataset.text = ''; |
| renderMarkdown(div); |
|
|
| const historyActions = document.createElement('div'); |
| historyActions.className = 'message-actions'; |
| const prevBtn = document.createElement('button'); |
| prevBtn.className = 'action-btn'; |
| prevBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M15 19l-7-7 7-7"></path></svg>'; |
| prevBtn.title = 'Previous Attempt'; |
| prevBtn.onclick = () => { |
| if (attemptCount > 1) { |
| attemptCount--; |
| div.dataset.text = attempts[attemptCount - 1]; |
| renderMarkdown(div); |
| } |
| }; |
| const nextBtn = document.createElement('button'); |
| nextBtn.className = 'action-btn'; |
| nextBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 5l7 7-7 7"></path></svg>'; |
| nextBtn.title = 'Next Attempt'; |
| nextBtn.onclick = () => { |
| if (attemptCount < attempts.length) { |
| attemptCount++; |
| div.dataset.text = attempts[attemptCount - 1]; |
| renderMarkdown(div); |
| } |
| }; |
| historyActions.appendChild(prevBtn); |
| historyActions.appendChild(document.createTextNode('Attempt ' + attemptCount)); |
| historyActions.appendChild(nextBtn); |
|
|
| container.appendChild(div); |
| container.appendChild(historyActions); |
| if (uiElements.chatBox) { |
| uiElements.chatBox.appendChild(container); |
| uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; |
| } else { |
| document.body.appendChild(container); |
| } |
| return div; |
| } |
|
|
| |
| |
| |
|
|
| async function submitMessage() { |
| if (isRequestActive || isRecording) return; |
| |
| let message = uiElements.input ? uiElements.input.value.trim() : ''; |
| |
| |
| if (message === lastSentMessage && Date.now() - lastSentTime < 2000) { |
| console.log('Duplicate message detected, ignoring'); |
| return; |
| } |
| lastSentMessage = message; |
| lastSentTime = Date.now(); |
| |
| let payload = null; |
| let formData = null; |
| let endpoint = '/api/chat'; |
| let headers = {}; |
|
|
| if (!message && (!uiElements.fileInput || uiElements.fileInput.files.length === 0) && (!uiElements.audioInput || uiElements.audioInput.files.length === 0)) { |
| return; |
| } |
|
|
| enterChatView(); |
|
|
| if (uiElements.fileInput && uiElements.fileInput.files.length > 0) { |
| const file = uiElements.fileInput.files[0]; |
| if (file.type.startsWith('image/')) { |
| endpoint = '/api/image-analysis'; |
| message = 'Analyze this image'; |
| formData = new FormData(); |
| formData.append('file', file); |
| formData.append('output_format', 'text'); |
| } |
| } else if (uiElements.audioInput && uiElements.audioInput.files.length > 0) { |
| const file = uiElements.audioInput.files[0]; |
| if (file.type.startsWith('audio/')) { |
| endpoint = '/api/audio-transcription'; |
| message = 'Transcribe this audio'; |
| formData = new FormData(); |
| formData.append('file', file); |
| } |
| } else if (message) { |
| const lang = detectLanguage(message); |
| const systemPrompts = { |
| 'ar': 'أنت مساعد ذكي تقدم إجابات مفصلة ومنظمة باللغة العربية، مع ضمان الدقة والوضوح.', |
| 'en': 'You are an expert assistant providing detailed, comprehensive, and well-structured responses.', |
| 'fr': 'Vous êtes un assistant expert fournissant des réponses détaillées, complètes et bien structurées.', |
| 'es': 'Eres un asistente experto que proporciona respuestas detalladas, completas y bien estructuradas.', |
| 'de': 'Sie sind ein Expertenassistent, der detaillierte, umfassende und gut strukturierte Antworten liefert.' |
| }; |
| const authResult = await checkAuth(); |
| |
| |
| let historyForPayload = []; |
| if (!authResult.authenticated && conversationHistory.length > 0) { |
| let limited = conversationHistory.slice(-15); |
| for (let i = 0; i < limited.length; i++) { |
| if (i === 0 || |
| limited[i].role !== limited[i-1].role || |
| limited[i].content !== limited[i-1].content) { |
| historyForPayload.push(limited[i]); |
| } |
| } |
| } |
| |
| payload = { |
| message: message, |
| system_prompt: systemPrompts[lang] || systemPrompts['en'], |
| history: historyForPayload, |
| temperature: 0.7, |
| max_new_tokens: 128000, |
| enable_browsing: true, |
| output_format: 'text' |
| }; |
| headers['Content-Type'] = 'application/json'; |
| } |
|
|
| addMsg('user', message); |
| const authResult = await checkAuth(); |
| if (!authResult.authenticated) { |
| conversationHistory.push({ role: 'user', content: message }); |
| sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
| } |
| streamMsg = addMsg('assistant', ''); |
| const thinkingEl = document.createElement('span'); |
| thinkingEl.className = 'thinking'; |
| thinkingEl.textContent = 'The model is thinking...'; |
| streamMsg.appendChild(thinkingEl); |
| updateUIForRequest(); |
|
|
| isRequestActive = true; |
| abortController = new AbortController(); |
| const startTime = Date.now(); |
|
|
| try { |
| const response = await sendRequest(endpoint, payload ? JSON.stringify(payload) : formData, headers); |
| let responseText = ''; |
| |
| if (endpoint === '/api/audio-transcription') { |
| const data = await response.json(); |
| if (!data.transcription) throw new Error('No transcription received from server'); |
| responseText = data.transcription; |
| streamMsg.dataset.text = responseText; |
| renderMarkdown(streamMsg); |
| streamMsg.dataset.done = '1'; |
| } else if (endpoint === '/api/image-analysis') { |
| const data = await response.json(); |
| responseText = data.image_analysis || 'Error: No analysis generated.'; |
| streamMsg.dataset.text = responseText; |
| renderMarkdown(streamMsg); |
| streamMsg.dataset.done = '1'; |
| } else { |
| const contentType = response.headers.get('Content-Type'); |
| if (contentType && contentType.includes('application/json')) { |
| const data = await response.json(); |
| responseText = data.response || 'Error: No response generated.'; |
| if (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 = ''; |
| streamMsg.dataset.text = ''; |
| if (streamMsg.querySelector('.thinking')) streamMsg.querySelector('.thinking').remove(); |
|
|
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) { |
| if (!buffer.trim()) throw new Error('Empty response from server'); |
| break; |
| } |
| const chunk = decoder.decode(value, { stream: true }); |
| buffer += chunk; |
| if (streamMsg) { |
| streamMsg.dataset.text = buffer; |
| currentAssistantText = buffer; |
| await renderMarkdown(streamMsg, true); |
| if (uiElements.chatBox) { |
| uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; |
| } |
| } |
| } |
| responseText = buffer; |
| } |
| } |
|
|
| const endTime = Date.now(); |
| const thinkingTime = Math.round((endTime - startTime) / 1000); |
| if (streamMsg) { |
| streamMsg.dataset.text = responseText + '\n\n*Processed in ' + thinkingTime + ' seconds.*'; |
| renderMarkdown(streamMsg); |
| streamMsg.dataset.done = '1'; |
| } |
| |
| const authResult2 = await checkAuth(); |
| if (!authResult2.authenticated) { |
| conversationHistory.push({ role: 'assistant', content: responseText }); |
| sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
| } |
| finalizeRequest(); |
| } catch (error) { |
| handleRequestError(error); |
| } |
| } |
|
|
| |
| |
| |
|
|
| function stopStream(forceCancel = false) { |
| if (!isRequestActive && !isRecording) return; |
| if (isRecording) stopVoiceRecording(); |
| isRequestActive = false; |
| if (abortController) { |
| abortController.abort(); |
| abortController = null; |
| } |
| if (streamMsg && !forceCancel) { |
| if (streamMsg.querySelector('.loading')) streamMsg.querySelector('.loading').remove(); |
| 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'; |
| enterChatView(); |
| } |
|
|
| |
| |
| |
|
|
| if (uiElements.settingsBtn) { |
| uiElements.settingsBtn.addEventListener('click', async () => { |
| const authResult = await checkAuth(); |
| if (!authResult.authenticated) { |
| alert('Please log in to access settings.'); |
| window.location.href = '/login'; |
| return; |
| } |
| try { |
| const response = await fetch('/api/settings', { |
| headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') } |
| }); |
| if (!response.ok) { |
| if (response.status === 401) { |
| localStorage.removeItem('token'); |
| window.location.href = '/login'; |
| } |
| throw new Error('Failed to fetch settings'); |
| } |
| const data = await response.json(); |
| |
| const displayNameField = document.getElementById('display_name'); |
| const preferredModelField = document.getElementById('preferred_model'); |
| const jobTitleField = document.getElementById('job_title'); |
| const educationField = document.getElementById('education'); |
| const interestsField = document.getElementById('interests'); |
| const additionalInfoField = document.getElementById('additional_info'); |
| const conversationStyleField = document.getElementById('conversation_style'); |
| |
| if (displayNameField) displayNameField.value = data.user_settings.display_name || ''; |
| if (preferredModelField) preferredModelField.value = data.user_settings.preferred_model || 'standard'; |
| if (jobTitleField) jobTitleField.value = data.user_settings.job_title || ''; |
| if (educationField) educationField.value = data.user_settings.education || ''; |
| if (interestsField) interestsField.value = data.user_settings.interests || ''; |
| if (additionalInfoField) additionalInfoField.value = data.user_settings.additional_info || ''; |
| if (conversationStyleField) conversationStyleField.value = data.user_settings.conversation_style || 'default'; |
|
|
| if (preferredModelField) { |
| preferredModelField.innerHTML = ''; |
| data.available_models.forEach(model => { |
| const option = document.createElement('option'); |
| option.value = model.alias; |
| option.textContent = model.alias + ' - ' + model.description; |
| preferredModelField.appendChild(option); |
| }); |
| } |
|
|
| if (conversationStyleField) { |
| conversationStyleField.innerHTML = ''; |
| data.conversation_styles.forEach(style => { |
| const option = document.createElement('option'); |
| option.value = style; |
| option.textContent = style.charAt(0).toUpperCase() + style.slice(1); |
| conversationStyleField.appendChild(option); |
| }); |
| } |
|
|
| if (uiElements.settingsModal) uiElements.settingsModal.classList.remove('hidden'); |
| toggleSidebar(false); |
| } catch (err) { |
| console.error('Error fetching settings:', err); |
| alert('Failed to load settings. Please try again.'); |
| } |
| }); |
| } |
|
|
| if (uiElements.closeSettingsBtn) { |
| uiElements.closeSettingsBtn.addEventListener('click', () => { |
| if (uiElements.settingsModal) uiElements.settingsModal.classList.add('hidden'); |
| }); |
| } |
|
|
| if (uiElements.cancelSettingsBtn) { |
| uiElements.cancelSettingsBtn.addEventListener('click', () => { |
| if (uiElements.settingsModal) uiElements.settingsModal.classList.add('hidden'); |
| }); |
| } |
|
|
| if (uiElements.settingsForm) { |
| uiElements.settingsForm.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| (async () => { |
| const authResult = await checkAuth(); |
| if (!authResult.authenticated) { |
| alert('Please log in to save settings.'); |
| window.location.href = '/login'; |
| return; |
| } |
| const formData = new FormData(uiElements.settingsForm); |
| const data = {}; |
| for (let pair of formData.entries()) { |
| data[pair[0]] = pair[1]; |
| } |
| try { |
| const response = await fetch('/users/me', { |
| method: 'PUT', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': 'Bearer ' + localStorage.getItem('token') |
| }, |
| body: JSON.stringify(data) |
| }); |
| if (!response.ok) { |
| if (response.status === 401) { |
| localStorage.removeItem('token'); |
| window.location.href = '/login'; |
| } |
| throw new Error('Failed to update settings'); |
| } |
| alert('Settings updated successfully!'); |
| if (uiElements.settingsModal) uiElements.settingsModal.classList.add('hidden'); |
| toggleSidebar(false); |
| } catch (err) { |
| console.error('Error updating settings:', err); |
| alert('Error updating settings: ' + err.message); |
| } |
| })(); |
| }); |
| } |
|
|
| |
| |
| |
|
|
| if (uiElements.promptItems) { |
| 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', () => { if (uiElements.fileInput) uiElements.fileInput.click(); }); |
| if (uiElements.audioBtn) uiElements.audioBtn.addEventListener('click', () => { if (uiElements.audioInput) uiElements.audioInput.click(); }); |
| if (uiElements.fileInput) uiElements.fileInput.addEventListener('change', previewFile); |
| if (uiElements.audioInput) uiElements.audioInput.addEventListener('change', previewFile); |
|
|
| if (uiElements.sendBtn) { |
| let pressTimer; |
| const handleSendAction = (e) => { |
| e.preventDefault(); |
| if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return; |
| if ((uiElements.input && uiElements.input.value.trim()) || |
| (uiElements.fileInput && uiElements.fileInput.files.length > 0) || |
| (uiElements.audioInput && uiElements.audioInput.files.length > 0)) { |
| submitMessage(); |
| } else { |
| pressTimer = setTimeout(() => startVoiceRecording(), 500); |
| } |
| }; |
| const handlePressEnd = (e) => { |
| e.preventDefault(); |
| clearTimeout(pressTimer); |
| if (isRecording) stopVoiceRecording(); |
| }; |
| |
| const oldSendBtn = uiElements.sendBtn; |
| const newSendBtn = oldSendBtn.cloneNode(true); |
| oldSendBtn.parentNode.replaceChild(newSendBtn, oldSendBtn); |
| uiElements.sendBtn = newSendBtn; |
| |
| uiElements.sendBtn.addEventListener('click', handleSendAction); |
| uiElements.sendBtn.addEventListener('touchstart', handleSendAction); |
| uiElements.sendBtn.addEventListener('touchend', handlePressEnd); |
| uiElements.sendBtn.addEventListener('touchcancel', handlePressEnd); |
| } |
|
|
| if (uiElements.form) { |
| uiElements.form.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| if (!isRecording && uiElements.input && uiElements.input.value.trim()) { |
| submitMessage(); |
| } else if (!isRecording && ((uiElements.fileInput && uiElements.fileInput.files.length > 0) || (uiElements.audioInput && uiElements.audioInput.files.length > 0))) { |
| submitMessage(); |
| } |
| }); |
| } |
|
|
| if (uiElements.input) { |
| uiElements.input.addEventListener('input', () => { |
| updateSendButtonState(); |
| autoResizeTextarea(); |
| }); |
| uiElements.input.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| if (!isRecording && uiElements.sendBtn && !uiElements.sendBtn.disabled) submitMessage(); |
| } |
| }); |
| } |
|
|
| if (uiElements.stopBtn) { |
| uiElements.stopBtn.addEventListener('click', () => { |
| if (uiElements.stopBtn) uiElements.stopBtn.style.pointerEvents = 'none'; |
| stopStream(); |
| }); |
| } |
|
|
| if (uiElements.clearBtn) uiElements.clearBtn.addEventListener('click', clearAllMessages); |
|
|
| if (uiElements.conversationTitle) { |
| uiElements.conversationTitle.addEventListener('click', async () => { |
| const authResult = await checkAuth(); |
| if (!authResult.authenticated) 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', async () => { |
| const authResult = await checkAuth(); |
| if (!authResult.authenticated) { |
| alert('Please log in to create a new conversation.'); |
| window.location.href = '/login'; |
| return; |
| } |
| await createNewConversation(); |
| }); |
| } |
|
|
| if (uiElements.historyToggle) { |
| uiElements.historyToggle.addEventListener('click', () => { |
| if (uiElements.conversationList) { |
| uiElements.conversationList.classList.toggle('hidden'); |
| if (uiElements.historyToggle) { |
| uiElements.historyToggle.innerHTML = uiElements.conversationList.classList.contains('hidden') |
| ? '<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>Show History' |
| : '<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>Hide History'; |
| } |
| } |
| }); |
| } |
|
|
| |
| |
| |
|
|
| const logoutBtnElement = document.querySelector('#logoutBtn'); |
| if (logoutBtnElement) { |
| logoutBtnElement.addEventListener('click', async () => { |
| console.log('Logout button clicked'); |
| try { |
| const response = await fetch('/logout', { |
| method: 'POST', |
| credentials: 'include' |
| }); |
| if (response.ok) { |
| localStorage.removeItem('token'); |
| console.log('Token removed from localStorage'); |
| window.location.href = '/login'; |
| } else { |
| console.error('Logout failed:', response.status); |
| alert('Failed to log out. Please try again.'); |
| } |
| } catch (error) { |
| console.error('Logout error:', error); |
| alert('Error during logout: ' + error.message); |
| } |
| }); |
| } |
|
|
| |
| |
| |
|
|
| window.addEventListener('offline', () => { |
| if (uiElements.messageLimitWarning) { |
| uiElements.messageLimitWarning.classList.remove('hidden'); |
| uiElements.messageLimitWarning.textContent = 'You are offline. Some features may be limited.'; |
| } |
| }); |
|
|
| window.addEventListener('online', () => { |
| if (uiElements.messageLimitWarning) { |
| uiElements.messageLimitWarning.classList.add('hidden'); |
| } |
| }); |
|
|
| |
| |
| |
|
|
| window.addEventListener('load', async () => { |
| console.log('Chat page loaded, checking authentication'); |
| try { |
| if (typeof AOS !== 'undefined') { |
| AOS.init({ |
| duration: 800, |
| easing: 'ease-out-cubic', |
| once: true, |
| offset: 50, |
| }); |
| } |
|
|
| enterChatView(true); |
|
|
| const authResult = await checkAuth(); |
| const userInfoElement = document.getElementById('user-info'); |
| if (authResult.authenticated) { |
| console.log('User authenticated:', authResult.user); |
| if (userInfoElement) { |
| userInfoElement.textContent = 'Welcome, ' + authResult.user.email; |
| } |
| if (currentConversationId) { |
| console.log('Loading conversation with ID:', currentConversationId); |
| await loadConversation(currentConversationId); |
| } |
| } else { |
| console.log('User not authenticated, handling as anonymous'); |
| if (userInfoElement) { |
| userInfoElement.textContent = 'Anonymous'; |
| } |
| await handleSession(); |
| if (conversationHistory.length > 0) { |
| console.log('Restoring conversation history'); |
| conversationHistory.forEach(msg => { |
| addMsg(msg.role, msg.content); |
| }); |
| } |
| } |
|
|
| await updateSidebarAfterLogin(); |
|
|
| autoResizeTextarea(); |
| updateSendButtonState(); |
| if (uiElements.swipeHint) { |
| setTimeout(() => { |
| uiElements.swipeHint.style.display = 'none'; |
| }, 3000); |
| } |
| setupTouchGestures(); |
| } catch (error) { |
| console.error('Error in window.load handler:', error); |
| } |
| }); |
|
|
| |
| const originalRemoveItem = localStorage.removeItem; |
| localStorage.removeItem = function(key) { |
| console.log('Removing from localStorage:', key); |
| originalRemoveItem.apply(this, arguments); |
| }; |