mgzon-app / static /js /chat.js
Mark-Lasfar
Update backend and server frontend for OAuth JSON response, client-side navigation, and add .gitignore
6905ba2
// SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
// 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 = `<div class="md-content" style="direction: ${isArabic ? 'rtl' : 'ltr'}; text-align: ${isArabic ? 'right' : 'left'};">${html}</div>`;
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 = '<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.title = 'Copy Code';
copyBtn.onclick = () => {
navigator.clipboard.writeText(pre.querySelector('code').innerText).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);
});
};
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 = '<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.title = 'Copy Response';
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.title = 'Retry';
retryBtn.onclick = () => submitMessage(); // إعادة إرسال السؤال
actions.appendChild(retryBtn);
}
// زر تعديل السؤال (للمستخدم فقط)
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.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 = `<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?.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) {
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 = `
<div class="flex items-center flex-1" style="direction: ${isArabicText(conv.title) ? '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.');
}
}
// 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 = '<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 {
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')
? `<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`;
}
});
}
// 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);
}