mgzon-app / static /js /chat.js
MGZON's picture
Update static/js/chat.js
17c7177 verified
// SPDX-FileCopyrightText: MGZon AI
// SPDX-License-Identifier: Apache-2.0
// MGZon Chat - Complete Unified Version with ALL Features (FINAL FIXED)
console.log('MGZon Chat v5.0 - FINAL FIXED version loaded at:', new Date().toISOString());
// ============================================================
// UI ELEMENTS
// ============================================================
const uiElements = {
chatArea: document.getElementById('chatArea'),
chatBox: document.getElementById('chatBox'),
initialContent: document.getElementById('initialContent'),
form: document.getElementById('footerForm'),
input: document.getElementById('userInput'),
sendBtn: document.getElementById('sendBtn'),
stopBtn: document.getElementById('stopBtn'),
fileBtn: document.getElementById('fileBtn'),
audioBtn: document.getElementById('audioBtn'),
fileInput: document.getElementById('fileInput'),
audioInput: document.getElementById('audioInput'),
filePreview: document.getElementById('filePreview'),
audioPreview: document.getElementById('audioPreview'),
promptItems: document.querySelectorAll('.prompt-item'),
chatHeader: document.getElementById('chatHeader'),
clearBtn: document.getElementById('clearBtn'),
messageLimitWarning: document.getElementById('messageLimitWarning'),
conversationTitle: document.getElementById('conversationTitle'),
sidebar: document.getElementById('sidebar'),
sidebarToggle: document.getElementById('sidebarToggle'),
conversationList: document.getElementById('conversationList'),
newConversationBtn: document.getElementById('newConversationBtn'),
swipeHint: document.getElementById('swipeHint'),
settingsBtn: document.getElementById('settingsBtn'),
settingsModal: document.getElementById('settingsModal'),
closeSettingsBtn: document.getElementById('closeSettingsBtn'),
cancelSettingsBtn: document.getElementById('cancelSettingsBtn'),
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;
let attemptCount = 0;
let attempts = [];
// منع إرسال نفس الرسالة مرتين
let lastSentMessage = '';
let lastSentTime = 0;
// ============================================================
// HELPER FUNCTIONS
// ============================================================
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);
}
// ============================================================
// AUTHENTICATION & SESSION
// ============================================================
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;
}
// ============================================================
// UPDATE SIDEBAR AFTER LOGIN
// ============================================================
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';
}
}
// ============================================================
// RENDER MARKDOWN
// ============================================================
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';
}
// ============================================================
// MESSAGE FUNCTIONS
// ============================================================
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';
}
// ============================================================
// CHAT VIEW FUNCTIONS
// ============================================================
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');
}
// ============================================================
// CLEAR MESSAGES
// ============================================================
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();
}
// ============================================================
// FILE & AUDIO PREVIEW
// ============================================================
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);
}
}
}
// ============================================================
// VOICE RECORDING
// ============================================================
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);
});
}
}
// ============================================================
// AUDIO MESSAGE
// ============================================================
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);
}
}
// ============================================================
// 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();
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();
}
// ============================================================
// CONVERSATION MANAGEMENT
// ============================================================
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.');
}
}
// ============================================================
// SIDEBAR & TOUCH GESTURES
// ============================================================
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);
});
}
// ============================================================
// ADD ATTEMPT HISTORY (Multiple responses feature)
// ============================================================
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;
}
// ============================================================
// SUBMIT MESSAGE - THE FIXED VERSION (مع منع تكرار الردود)
// ============================================================
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();
// تنظيف الـ history قبل الإرسال
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);
}
}
// ============================================================
// STOP STREAMING
// ============================================================
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();
}
// ============================================================
// SETTINGS MODAL
// ============================================================
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);
}
})();
});
}
// ============================================================
// EVENT LISTENERS
// ============================================================
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';
}
}
});
}
// ============================================================
// LOGOUT HANDLER
// ============================================================
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);
}
});
}
// ============================================================
// 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');
}
});
// ============================================================
// INITIALIZATION
// ============================================================
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);
}
});
// Debug localStorage
const originalRemoveItem = localStorage.removeItem;
localStorage.removeItem = function(key) {
console.log('Removing from localStorage:', key);
originalRemoveItem.apply(this, arguments);
};