// 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 with fallback check const uiElements = { chatArea: document.getElementById('chatArea') || document.createElement('div'), chatBox: document.getElementById('chatBox') || document.createElement('div'), initialContent: document.getElementById('initialContent') || document.createElement('div'), form: document.getElementById('footerForm') || document.createElement('form'), 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'), }; // State variables 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; async function checkAuth() { const urlParams = new URLSearchParams(window.location.search); const accessTokenFromUrl = urlParams.get('access_token'); if (accessTokenFromUrl) { console.log('Access token found in URL, saving to localStorage'); 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) { console.log('Access token found in cookie, saving to localStorage'); localStorage.setItem('token', token); } } if (!token) { console.log('No auth token found in localStorage or cookie'); 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') { console.log('Auth token verified, user:', data.user); return { authenticated: true, user: data.user }; } else { console.log('Token verification failed:', data.detail || 'Invalid token'); localStorage.removeItem('token'); if (typeof Cookies !== 'undefined') { Cookies.remove('fastapiusersauth'); } return { authenticated: false, user: null }; } } catch (error) { console.error('Error verifying token:', error); localStorage.removeItem('token'); if (typeof Cookies !== 'undefined') { Cookies.remove('fastapiusersauth'); } return { authenticated: false, user: null }; } } async function handleSession() { const sessionId = sessionStorage.getItem('session_id'); if (!sessionId) { const newSessionId = crypto.randomUUID(); sessionStorage.setItem('session_id', newSessionId); console.log('New session_id created:', newSessionId); return newSessionId; } console.log('Existing session_id:', sessionId); return sessionId; } window.addEventListener('load', async () => { console.log('Chat page loaded, checking authentication'); try { 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}`; } else { console.warn('user-info element not found'); } if (typeof currentConversationId !== 'undefined' && currentConversationId) { console.log('Authenticated user, loading conversation with ID:', currentConversationId); await loadConversation(currentConversationId); } } else { console.log('User not authenticated, handling as anonymous'); if (userInfoElement) { userInfoElement.textContent = 'Anonymous'; } else { console.warn('user-info element not found'); } await handleSession(); if (typeof conversationHistory !== 'undefined' && conversationHistory.length > 0) { console.log('Restoring conversation history from sessionStorage:', conversationHistory); conversationHistory.forEach(msg => { console.log('Adding message from history:', msg); addMsg(msg.role, msg.content); }); } else { console.log('No conversation history, starting fresh'); } } autoResizeTextarea(); updateSendButtonState(); if (uiElements.swipeHint) { setTimeout(() => { uiElements.swipeHint.style.display = 'none'; }, 3000); } else { console.warn('swipeHint element not found'); } setupTouchGestures(); } catch (error) { console.error('Error in window.load handler:', error); } }); // Update send button state 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; console.log('Send button state updated:', { hasInput, isRequestActive, isRecording, disabled: uiElements.sendBtn.disabled }); } else { console.warn('One or more uiElements are missing:', uiElements); } } // 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('pre').forEach(pre => { const copyBtn = document.createElement('button'); copyBtn.className = 'copy-btn'; copyBtn.innerHTML = ''; copyBtn.title = 'Copy Code'; copyBtn.onclick = () => { navigator.clipboard.writeText(pre.querySelector('code').innerText).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.innerHTML = ''; }, 2000); }); }; pre.appendChild(copyBtn); }); wrapper.querySelectorAll('hr').forEach(h => h.classList.add('styled-hr')); Prism.highlightAllUnder(wrapper); if (uiElements.chatBox) { uiElements.chatBox.scrollTo({ top: uiElements.chatBox.scrollHeight, behavior: 'smooth', }); } el.style.display = 'block'; } // Toggle chat view with force option function enterChatView(force = false) { if (uiElements.chatHeader) { uiElements.chatHeader.classList.remove('hidden'); uiElements.chatHeader.setAttribute('aria-hidden', 'false'); if (currentConversationTitle && uiElements.conversationTitle) { uiElements.conversationTitle.textContent = currentConversationTitle; } } if (uiElements.chatArea) { uiElements.chatArea.classList.remove('hidden'); uiElements.chatArea.style.display = force ? 'flex !important' : 'flex'; uiElements.chatArea.style.opacity = '1'; uiElements.chatArea.style.visibility = 'visible'; } if (uiElements.chatBox) { uiElements.chatBox.classList.remove('hidden'); uiElements.chatBox.style.display = force ? 'flex !important' : 'flex'; uiElements.chatBox.style.opacity = '1'; uiElements.chatBox.style.visibility = 'visible'; } if (uiElements.initialContent) uiElements.initialContent.classList.add('hidden'); if (uiElements.form) { uiElements.form.classList.remove('hidden'); uiElements.form.style.display = force ? 'flex !important' : 'flex'; uiElements.form.style.opacity = '1'; uiElements.form.style.visibility = 'visible'; } console.log('Chat view forced to enter:', { chatArea: uiElements.chatArea?.style.display, chatBox: uiElements.chatBox?.style.display, form: uiElements.form?.style.display }); } // 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'); if (uiElements.form) uiElements.form.classList.add('hidden'); } // Add chat bubble function addMsg(who, text) { const container = document.createElement('div'); container.className = 'message-container'; const div = document.createElement('div'); div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`; div.dataset.text = text; console.log('Adding message:', { who, 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 = ''; copyBtn.title = 'Copy Response'; copyBtn.onclick = () => { navigator.clipboard.writeText(text).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.innerHTML = ''; }, 2000); }); }; actions.appendChild(copyBtn); // زر إعادة المحاولة (للمساعد فقط) if (who === 'assistant') { const retryBtn = document.createElement('button'); retryBtn.className = 'action-btn'; retryBtn.innerHTML = ''; retryBtn.title = 'Retry'; retryBtn.onclick = () => submitMessage(); // إعادة إرسال السؤال actions.appendChild(retryBtn); } // زر تعديل السؤال (للمستخدم فقط) if (who === 'user') { const editBtn = document.createElement('button'); editBtn.className = 'action-btn'; editBtn.innerHTML = ''; editBtn.title = 'Edit Question'; editBtn.onclick = () => { const newText = prompt('Edit your question:', text); if (newText) { div.dataset.text = newText; renderMarkdown(div); conversationHistory = conversationHistory.map(msg => msg.role === 'user' && msg.content === text ? { role: 'user', content: newText } : msg); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); } }; actions.appendChild(editBtn); } container.appendChild(div); container.appendChild(actions); if (uiElements.chatBox) { uiElements.chatBox.appendChild(container); uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; } else { console.error('chatBox not found, appending to a fallback container'); document.body.appendChild(container); } return div; } // Clear all messages function clearAllMessages() { stopStream(true); conversationHistory = []; sessionStorage.removeItem('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) { console.log('Voice recording blocked: Request active or already recording'); 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(); console.log('MediaRecorder started'); mediaRecorder.addEventListener('dataavailable', event => { audioChunks.push(event.data); console.log('Audio chunk received:', 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 () => { console.log('Stopping voice recording, sending audio...'); 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'); if (!(await checkAuth())) { 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'; } if (!(await checkAuth())) { conversationHistory.push({ role: 'assistant', content: transcription }); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); } 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 = localStorage.getItem('token'); if (token) headers['Authorization'] = `Bearer ${token}`; headers['X-Session-ID'] = await handleSession(); console.log('Sending request to:', endpoint, 'with headers:', headers); 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) { console.error('Send request error:', 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'; uiElements.sendBtn.disabled = false; } if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; updateSendButtonState(); } // 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.'}`; 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; if (!(checkAuth())) { 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(); } // Load conversations for sidebar async function loadConversations() { if (!(await checkAuth())) 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'); 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 ${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.'); } } // 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 ${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.'); } } // Create new conversation async function createNewConversation() { if (!(await 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 ${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.scrollTo({ top: uiElements.chatBox.scrollHeight, behavior: 'smooth', }); } // 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 ${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.'); } } // 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)`; 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); } }); } // 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'; if (!message && !uiElements.fileInput?.files.length && !uiElements.audioInput?.files.length) { console.log('No message, file, or audio to send'); return; } enterChatView(); // دايمًا إظهار المحادثة قبل الإرسال 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: (await checkAuth()) ? [] : conversationHistory, temperature: 0.7, max_new_tokens: 128000, enable_browsing: true, output_format: 'text' }; headers['Content-Type'] = 'application/json'; } addMsg('user', message); if (!(await checkAuth())) { 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 || 'Error: No transcription generated.'; 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?.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 = ''; 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; console.log('Received chunk:', chunk); if (streamMsg) { streamMsg.dataset.text = buffer; currentAssistantText = buffer; renderMarkdown(streamMsg); streamMsg.style.opacity = '1'; if (uiElements.chatBox) { uiElements.chatBox.style.display = 'flex'; uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; } await new Promise(resolve => setTimeout(resolve, 30)); // تأخير 30ms لتأثير الكتابة الطبيعي } } responseText = buffer; } } const endTime = Date.now(); const thinkingTime = Math.round((endTime - startTime) / 1000); // حساب الوقت بالثواني streamMsg.dataset.text += `\n\n*Processed in ${thinkingTime} seconds.*`; if (streamMsg) { streamMsg.dataset.text = responseText; renderMarkdown(streamMsg); streamMsg.dataset.done = '1'; } if (!(await checkAuth())) { conversationHistory.push({ role: 'assistant', content: responseText }); sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); } finalizeRequest(); } catch (error) { handleRequestError(error); } } let attemptCount = 0; let attempts = []; // لتخزين الردود السابقة function addAttemptHistory(who, text) { attemptCount++; attempts.push(text); // تخزين الرد const container = document.createElement('div'); container.className = 'message-container'; const div = document.createElement('div'); div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`; 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 = ''; 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 = ''; 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 { console.error('chatBox not found, appending to a fallback container'); document.body.appendChild(container); } return div; } // 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'; enterChatView(); } // Logout handler const logoutBtn = document.querySelector('#logoutBtn'); if (logoutBtn) { logoutBtn.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); } }); } // Settings Modal if (uiElements.settingsBtn) { uiElements.settingsBtn.addEventListener('click', async () => { if (!(await checkAuth())) { 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(); 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); }); 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', () => { 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 ${localStorage.getItem('token')}` }, 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) { let pressTimer; const handleSendAction = (e) => { e.preventDefault(); if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return; if (uiElements.input.value.trim() || uiElements.fileInput.files.length > 0 || uiElements.audioInput.files.length > 0) { submitMessage(); } else { pressTimer = setTimeout(() => startVoiceRecording(), 500); } }; const handlePressEnd = (e) => { e.preventDefault(); clearTimeout(pressTimer); if (isRecording) stopVoiceRecording(); }; uiElements.sendBtn.replaceWith(uiElements.sendBtn.cloneNode(true)); uiElements.sendBtn = document.getElementById('sendBtn'); 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.value.trim()) { submitMessage(); } else if (!isRecording && (uiElements.fileInput.files.length > 0 || 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.disabled) submitMessage(); } }); } 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', async () => { if (!(await checkAuth())) { alert('Please log in to create a new conversation.'); window.location.href = '/login'; return; } await createNewConversation(); }); } // Debug localStorage const originalRemoveItem = localStorage.removeItem; localStorage.removeItem = function (key) { console.log('Removing from localStorage:', key); originalRemoveItem.apply(this, arguments); }; // Offline mode detection 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'); } }); // Function to auto-resize textarea function autoResizeTextarea() { if (uiElements.input) { uiElements.input.style.height = 'auto'; uiElements.input.style.height = `${uiElements.input.scrollHeight}px`; updateSendButtonState(); } } // Function to check if text contains Arabic characters function isArabicText(text) { return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text); }