Mark-Lasfar
commited on
Commit
·
72a556f
1
Parent(s):
596833b
add chat
Browse files- static/js/chat.js +185 -130
static/js/chat.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
// SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
|
| 2 |
// SPDX-License-Identifier: Apache-2.0
|
| 3 |
|
| 4 |
-
// Prism
|
| 5 |
Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/';
|
| 6 |
|
| 7 |
-
//
|
| 8 |
const uiElements = {
|
| 9 |
chatArea: document.getElementById('chatArea'),
|
| 10 |
chatBox: document.getElementById('chatBox'),
|
|
@@ -36,7 +36,7 @@ const uiElements = {
|
|
| 36 |
historyToggle: document.getElementById('historyToggle'),
|
| 37 |
};
|
| 38 |
|
| 39 |
-
//
|
| 40 |
let conversationHistory = JSON.parse(sessionStorage.getItem('conversationHistory') || '[]');
|
| 41 |
let currentConversationId = window.conversationId || null;
|
| 42 |
let currentConversationTitle = window.conversationTitle || null;
|
|
@@ -48,7 +48,7 @@ let streamMsg = null;
|
|
| 48 |
let currentAssistantText = '';
|
| 49 |
let isSidebarOpen = window.innerWidth >= 768;
|
| 50 |
|
| 51 |
-
//
|
| 52 |
document.addEventListener('DOMContentLoaded', async () => {
|
| 53 |
AOS.init({
|
| 54 |
duration: 800,
|
|
@@ -57,13 +57,19 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
| 57 |
offset: 50,
|
| 58 |
});
|
| 59 |
|
|
|
|
| 60 |
if (currentConversationId && checkAuth()) {
|
|
|
|
| 61 |
await loadConversation(currentConversationId);
|
| 62 |
} else if (conversationHistory.length > 0) {
|
|
|
|
| 63 |
enterChatView();
|
| 64 |
conversationHistory.forEach(msg => {
|
|
|
|
| 65 |
addMsg(msg.role, msg.content);
|
| 66 |
});
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
autoResizeTextarea();
|
|
@@ -74,52 +80,40 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
| 74 |
}, 3000);
|
| 75 |
}
|
| 76 |
setupTouchGestures();
|
| 77 |
-
await loadConversations(); // Load conversations always if authenticated
|
| 78 |
});
|
| 79 |
|
| 80 |
-
//
|
| 81 |
function checkAuth() {
|
| 82 |
const token = localStorage.getItem('token');
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
fetch('/api/verify-token', {
|
| 86 |
-
headers: { 'Authorization': `Bearer ${token}` }
|
| 87 |
-
}).then(res => {
|
| 88 |
-
if (!res.ok) {
|
| 89 |
-
localStorage.removeItem('token');
|
| 90 |
-
return false;
|
| 91 |
-
}
|
| 92 |
-
return true;
|
| 93 |
-
}).catch(() => {
|
| 94 |
-
localStorage.removeItem('token');
|
| 95 |
-
return false;
|
| 96 |
-
});
|
| 97 |
-
}
|
| 98 |
-
return !!token;
|
| 99 |
}
|
| 100 |
|
| 101 |
-
//
|
| 102 |
async function handleSession() {
|
| 103 |
const sessionId = sessionStorage.getItem('session_id');
|
| 104 |
if (!sessionId) {
|
| 105 |
const newSessionId = crypto.randomUUID();
|
| 106 |
sessionStorage.setItem('session_id', newSessionId);
|
|
|
|
| 107 |
return newSessionId;
|
| 108 |
}
|
|
|
|
| 109 |
return sessionId;
|
| 110 |
}
|
| 111 |
|
| 112 |
-
//
|
| 113 |
function updateSendButtonState() {
|
| 114 |
if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) {
|
| 115 |
-
const hasInput = uiElements.input.value.trim() !== '' ||
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
uiElements.sendBtn.disabled = !hasInput;
|
|
|
|
| 119 |
}
|
| 120 |
}
|
| 121 |
|
| 122 |
-
//
|
| 123 |
function renderMarkdown(el) {
|
| 124 |
const raw = el.dataset.text || '';
|
| 125 |
const isArabic = isArabicText(raw);
|
|
@@ -150,7 +144,7 @@ function renderMarkdown(el) {
|
|
| 150 |
}
|
| 151 |
}
|
| 152 |
|
| 153 |
-
//
|
| 154 |
function enterChatView() {
|
| 155 |
if (uiElements.chatHeader) {
|
| 156 |
uiElements.chatHeader.classList.remove('hidden');
|
|
@@ -163,7 +157,7 @@ function enterChatView() {
|
|
| 163 |
if (uiElements.initialContent) uiElements.initialContent.classList.add('hidden');
|
| 164 |
}
|
| 165 |
|
| 166 |
-
//
|
| 167 |
function leaveChatView() {
|
| 168 |
if (uiElements.chatHeader) {
|
| 169 |
uiElements.chatHeader.classList.add('hidden');
|
|
@@ -173,20 +167,23 @@ function leaveChatView() {
|
|
| 173 |
if (uiElements.initialContent) uiElements.initialContent.classList.remove('hidden');
|
| 174 |
}
|
| 175 |
|
| 176 |
-
//
|
| 177 |
function addMsg(who, text) {
|
| 178 |
const div = document.createElement('div');
|
| 179 |
div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`;
|
| 180 |
div.dataset.text = text;
|
|
|
|
| 181 |
renderMarkdown(div);
|
| 182 |
if (uiElements.chatBox) {
|
| 183 |
uiElements.chatBox.appendChild(div);
|
| 184 |
uiElements.chatBox.classList.remove('hidden');
|
|
|
|
|
|
|
| 185 |
}
|
| 186 |
return div;
|
| 187 |
}
|
| 188 |
|
| 189 |
-
//
|
| 190 |
function clearAllMessages() {
|
| 191 |
stopStream(true);
|
| 192 |
conversationHistory = [];
|
|
@@ -211,7 +208,7 @@ function clearAllMessages() {
|
|
| 211 |
autoResizeTextarea();
|
| 212 |
}
|
| 213 |
|
| 214 |
-
//
|
| 215 |
function previewFile() {
|
| 216 |
if (uiElements.fileInput?.files.length > 0) {
|
| 217 |
const file = uiElements.fileInput.files[0];
|
|
@@ -245,19 +242,26 @@ function previewFile() {
|
|
| 245 |
}
|
| 246 |
}
|
| 247 |
|
| 248 |
-
//
|
| 249 |
function startVoiceRecording() {
|
| 250 |
-
if (isRequestActive || isRecording)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
isRecording = true;
|
| 252 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.add('recording');
|
| 253 |
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
|
| 254 |
mediaRecorder = new MediaRecorder(stream);
|
| 255 |
audioChunks = [];
|
| 256 |
mediaRecorder.start();
|
|
|
|
| 257 |
mediaRecorder.addEventListener('dataavailable', event => {
|
| 258 |
audioChunks.push(event.data);
|
|
|
|
| 259 |
});
|
| 260 |
}).catch(err => {
|
|
|
|
| 261 |
alert('Failed to access microphone. Please check permissions.');
|
| 262 |
isRecording = false;
|
| 263 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
|
|
@@ -270,6 +274,7 @@ function stopVoiceRecording() {
|
|
| 270 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
|
| 271 |
isRecording = false;
|
| 272 |
mediaRecorder.addEventListener('stop', async () => {
|
|
|
|
| 273 |
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
| 274 |
const formData = new FormData();
|
| 275 |
formData.append('file', audioBlob, 'voice-message.webm');
|
|
@@ -278,7 +283,7 @@ function stopVoiceRecording() {
|
|
| 278 |
}
|
| 279 |
}
|
| 280 |
|
| 281 |
-
//
|
| 282 |
async function submitAudioMessage(formData) {
|
| 283 |
enterChatView();
|
| 284 |
addMsg('user', 'Voice message');
|
|
@@ -324,11 +329,12 @@ async function submitAudioMessage(formData) {
|
|
| 324 |
}
|
| 325 |
}
|
| 326 |
|
| 327 |
-
//
|
| 328 |
async function sendRequest(endpoint, body, headers = {}) {
|
| 329 |
-
const token =
|
| 330 |
if (token) headers['Authorization'] = `Bearer ${token}`;
|
| 331 |
headers['X-Session-ID'] = await handleSession();
|
|
|
|
| 332 |
try {
|
| 333 |
const response = await fetch(endpoint, {
|
| 334 |
method: 'POST',
|
|
@@ -353,6 +359,7 @@ async function sendRequest(endpoint, body, headers = {}) {
|
|
| 353 |
}
|
| 354 |
return response;
|
| 355 |
} catch (error) {
|
|
|
|
| 356 |
if (error.name === 'AbortError') {
|
| 357 |
throw new Error('Request was aborted');
|
| 358 |
}
|
|
@@ -360,7 +367,7 @@ async function sendRequest(endpoint, body, headers = {}) {
|
|
| 360 |
}
|
| 361 |
}
|
| 362 |
|
| 363 |
-
//
|
| 364 |
function updateUIForRequest() {
|
| 365 |
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'inline-flex';
|
| 366 |
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'none';
|
|
@@ -371,7 +378,7 @@ function updateUIForRequest() {
|
|
| 371 |
autoResizeTextarea();
|
| 372 |
}
|
| 373 |
|
| 374 |
-
//
|
| 375 |
function finalizeRequest() {
|
| 376 |
streamMsg = null;
|
| 377 |
isRequestActive = false;
|
|
@@ -380,7 +387,7 @@ function finalizeRequest() {
|
|
| 380 |
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
|
| 381 |
}
|
| 382 |
|
| 383 |
-
//
|
| 384 |
function handleRequestError(error) {
|
| 385 |
if (streamMsg) {
|
| 386 |
streamMsg.querySelector('.loading')?.remove();
|
|
@@ -394,6 +401,7 @@ function handleRequestError(error) {
|
|
| 394 |
streamMsg.dataset.done = '1';
|
| 395 |
streamMsg = null;
|
| 396 |
}
|
|
|
|
| 397 |
alert(`Error: ${error.message || 'An error occurred during the request.'}`);
|
| 398 |
isRequestActive = false;
|
| 399 |
abortController = null;
|
|
@@ -402,12 +410,12 @@ function handleRequestError(error) {
|
|
| 402 |
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
|
| 403 |
}
|
| 404 |
|
| 405 |
-
//
|
| 406 |
async function loadConversations() {
|
| 407 |
if (!checkAuth()) return;
|
| 408 |
try {
|
| 409 |
const response = await fetch('/api/conversations', {
|
| 410 |
-
headers: { 'Authorization': `Bearer ${
|
| 411 |
});
|
| 412 |
if (!response.ok) throw new Error('Failed to load conversations');
|
| 413 |
const conversations = await response.json();
|
|
@@ -436,18 +444,22 @@ async function loadConversations() {
|
|
| 436 |
});
|
| 437 |
}
|
| 438 |
} catch (error) {
|
|
|
|
| 439 |
alert('Failed to load conversations. Please try again.');
|
| 440 |
}
|
| 441 |
}
|
| 442 |
|
| 443 |
-
//
|
| 444 |
async function loadConversation(conversationId) {
|
| 445 |
try {
|
| 446 |
const response = await fetch(`/api/conversations/${conversationId}`, {
|
| 447 |
-
headers: { 'Authorization': `Bearer ${
|
| 448 |
});
|
| 449 |
if (!response.ok) {
|
| 450 |
-
if (response.status === 401)
|
|
|
|
|
|
|
|
|
|
| 451 |
throw new Error('Failed to load conversation');
|
| 452 |
}
|
| 453 |
const data = await response.json();
|
|
@@ -461,20 +473,24 @@ async function loadConversation(conversationId) {
|
|
| 461 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 462 |
toggleSidebar(false);
|
| 463 |
} catch (error) {
|
|
|
|
| 464 |
alert('Failed to load conversation. Please try again or log in.');
|
| 465 |
}
|
| 466 |
}
|
| 467 |
|
| 468 |
-
//
|
| 469 |
async function deleteConversation(conversationId) {
|
| 470 |
if (!confirm('Are you sure you want to delete this conversation?')) return;
|
| 471 |
try {
|
| 472 |
const response = await fetch(`/api/conversations/${conversationId}`, {
|
| 473 |
method: 'DELETE',
|
| 474 |
-
headers: { 'Authorization': `Bearer ${
|
| 475 |
});
|
| 476 |
if (!response.ok) {
|
| 477 |
-
if (response.status === 401)
|
|
|
|
|
|
|
|
|
|
| 478 |
throw new Error('Failed to delete conversation');
|
| 479 |
}
|
| 480 |
if (conversationId === currentConversationId) {
|
|
@@ -485,30 +501,38 @@ async function deleteConversation(conversationId) {
|
|
| 485 |
}
|
| 486 |
await loadConversations();
|
| 487 |
} catch (error) {
|
|
|
|
| 488 |
alert('Failed to delete conversation. Please try again.');
|
| 489 |
}
|
| 490 |
}
|
| 491 |
|
| 492 |
-
//
|
| 493 |
async function saveMessageToConversation(conversationId, role, content) {
|
| 494 |
try {
|
| 495 |
const response = await fetch(`/api/conversations/${conversationId}`, {
|
| 496 |
method: 'POST',
|
| 497 |
headers: {
|
| 498 |
'Content-Type': 'application/json',
|
| 499 |
-
'Authorization': `Bearer ${
|
| 500 |
},
|
| 501 |
body: JSON.stringify({ role, content })
|
| 502 |
});
|
| 503 |
-
if (!response.ok)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
} catch (error) {
|
| 505 |
console.error('Error saving message:', error);
|
| 506 |
}
|
| 507 |
}
|
| 508 |
|
| 509 |
-
//
|
| 510 |
async function createNewConversation() {
|
| 511 |
if (!checkAuth()) {
|
|
|
|
| 512 |
window.location.href = '/login';
|
| 513 |
return;
|
| 514 |
}
|
|
@@ -517,7 +541,7 @@ async function createNewConversation() {
|
|
| 517 |
method: 'POST',
|
| 518 |
headers: {
|
| 519 |
'Content-Type': 'application/json',
|
| 520 |
-
'Authorization': `Bearer ${
|
| 521 |
},
|
| 522 |
body: JSON.stringify({ title: 'New Conversation' })
|
| 523 |
});
|
|
@@ -540,6 +564,7 @@ async function createNewConversation() {
|
|
| 540 |
await loadConversations();
|
| 541 |
toggleSidebar(false);
|
| 542 |
} catch (error) {
|
|
|
|
| 543 |
alert('Failed to create new conversation. Please try again.');
|
| 544 |
}
|
| 545 |
if (uiElements.chatBox) uiElements.chatBox.scrollTo({
|
|
@@ -548,57 +573,50 @@ async function createNewConversation() {
|
|
| 548 |
});
|
| 549 |
}
|
| 550 |
|
| 551 |
-
//
|
| 552 |
async function updateConversationTitle(conversationId, newTitle) {
|
| 553 |
try {
|
| 554 |
const response = await fetch(`/api/conversations/${conversationId}/title`, {
|
| 555 |
method: 'PUT',
|
| 556 |
headers: {
|
| 557 |
'Content-Type': 'application/json',
|
| 558 |
-
'Authorization': `Bearer ${
|
| 559 |
},
|
| 560 |
body: JSON.stringify({ title: newTitle })
|
| 561 |
});
|
| 562 |
-
if (!response.ok)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
const data = await response.json();
|
| 564 |
currentConversationTitle = data.title;
|
| 565 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 566 |
await loadConversations();
|
| 567 |
} catch (error) {
|
|
|
|
| 568 |
alert('Failed to update conversation title.');
|
| 569 |
}
|
| 570 |
}
|
| 571 |
|
| 572 |
-
//
|
| 573 |
-
function toggleSidebar(show) {
|
| 574 |
-
if (uiElements.sidebar) {
|
| 575 |
-
isSidebarOpen = show !== undefined ? show : !isSidebarOpen;
|
| 576 |
-
uiElements.sidebar.style.transform = isSidebarOpen ? 'translateX(0)' : 'translateX(-100%)';
|
| 577 |
-
if (uiElements.swipeHint && !isSidebarOpen) {
|
| 578 |
-
uiElements.swipeHint.style.display = 'block';
|
| 579 |
-
setTimeout(() => {
|
| 580 |
-
uiElements.swipeHint.style.display = 'none';
|
| 581 |
-
}, 3000);
|
| 582 |
-
} else if (uiElements.swipeHint) {
|
| 583 |
-
uiElements.swipeHint.style.display = 'none';
|
| 584 |
-
}
|
| 585 |
-
}
|
| 586 |
-
}
|
| 587 |
-
|
| 588 |
-
// Setup touch gestures with Hammer.js - optimized for smoothness
|
| 589 |
function setupTouchGestures() {
|
| 590 |
if (!uiElements.sidebar) return;
|
| 591 |
-
const hammer = new Hammer(uiElements.sidebar
|
| 592 |
const mainContent = document.querySelector('.flex-1');
|
| 593 |
const hammerMain = new Hammer(mainContent);
|
| 594 |
|
| 595 |
-
hammer.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL
|
| 596 |
hammer.on('pan', e => {
|
| 597 |
if (!isSidebarOpen) return;
|
| 598 |
let translateX = Math.max(-uiElements.sidebar.offsetWidth, Math.min(0, e.deltaX));
|
| 599 |
uiElements.sidebar.style.transform = `translateX(${translateX}px)`;
|
|
|
|
| 600 |
});
|
| 601 |
hammer.on('panend', e => {
|
|
|
|
| 602 |
if (e.deltaX < -50) {
|
| 603 |
toggleSidebar(false);
|
| 604 |
} else {
|
|
@@ -606,14 +624,27 @@ function setupTouchGestures() {
|
|
| 606 |
}
|
| 607 |
});
|
| 608 |
|
| 609 |
-
hammerMain.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
hammerMain.on('pan', e => {
|
| 611 |
if (isSidebarOpen) return;
|
| 612 |
-
|
| 613 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
});
|
| 615 |
hammerMain.on('panend', e => {
|
| 616 |
-
|
|
|
|
|
|
|
|
|
|
| 617 |
toggleSidebar(true);
|
| 618 |
} else {
|
| 619 |
toggleSidebar(false);
|
|
@@ -621,21 +652,31 @@ function setupTouchGestures() {
|
|
| 621 |
});
|
| 622 |
}
|
| 623 |
|
| 624 |
-
//
|
| 625 |
async function submitMessage() {
|
| 626 |
-
if (isRequestActive || isRecording)
|
|
|
|
|
|
|
|
|
|
| 627 |
let message = uiElements.input?.value.trim() || '';
|
| 628 |
let payload = null;
|
| 629 |
let formData = null;
|
| 630 |
let endpoint = '/api/chat';
|
| 631 |
let headers = {};
|
|
|
|
|
|
|
|
|
|
| 632 |
|
| 633 |
-
if (!message && !uiElements.fileInput?.files.length && !uiElements.audioInput?.files.length)
|
|
|
|
|
|
|
|
|
|
| 634 |
|
| 635 |
if (uiElements.fileInput?.files.length > 0) {
|
| 636 |
const file = uiElements.fileInput.files[0];
|
| 637 |
if (file.type.startsWith('image/')) {
|
| 638 |
endpoint = '/api/image-analysis';
|
|
|
|
| 639 |
message = 'Analyze this image';
|
| 640 |
formData = new FormData();
|
| 641 |
formData.append('file', file);
|
|
@@ -645,6 +686,7 @@ async function submitMessage() {
|
|
| 645 |
const file = uiElements.audioInput.files[0];
|
| 646 |
if (file.type.startsWith('audio/')) {
|
| 647 |
endpoint = '/api/audio-transcription';
|
|
|
|
| 648 |
message = 'Transcribe this audio';
|
| 649 |
formData = new FormData();
|
| 650 |
formData.append('file', file);
|
|
@@ -659,7 +701,8 @@ async function submitMessage() {
|
|
| 659 |
temperature: 0.7,
|
| 660 |
max_new_tokens: 128000,
|
| 661 |
enable_browsing: true,
|
| 662 |
-
output_format: 'text'
|
|
|
|
| 663 |
};
|
| 664 |
headers['Content-Type'] = 'application/json';
|
| 665 |
}
|
|
@@ -682,6 +725,7 @@ async function submitMessage() {
|
|
| 682 |
let responseText = '';
|
| 683 |
if (endpoint === '/api/audio-transcription') {
|
| 684 |
const data = await response.json();
|
|
|
|
| 685 |
responseText = data.transcription || 'Error: No transcription generated.';
|
| 686 |
} else if (endpoint === '/api/image-analysis') {
|
| 687 |
const data = await response.json();
|
|
@@ -704,7 +748,10 @@ async function submitMessage() {
|
|
| 704 |
let buffer = '';
|
| 705 |
while (true) {
|
| 706 |
const { done, value } = await reader.read();
|
| 707 |
-
if (done)
|
|
|
|
|
|
|
|
|
|
| 708 |
buffer += decoder.decode(value, { stream: true });
|
| 709 |
if (streamMsg) {
|
| 710 |
streamMsg.dataset.text = buffer;
|
|
@@ -736,7 +783,7 @@ async function submitMessage() {
|
|
| 736 |
}
|
| 737 |
}
|
| 738 |
|
| 739 |
-
//
|
| 740 |
function stopStream(forceCancel = false) {
|
| 741 |
if (!isRequestActive && !isRecording) return;
|
| 742 |
if (isRecording) stopVoiceRecording();
|
|
@@ -757,10 +804,11 @@ function stopStream(forceCancel = false) {
|
|
| 757 |
if (uiElements.stopBtn) uiElements.stopBtn.style.pointerEvents = 'auto';
|
| 758 |
}
|
| 759 |
|
| 760 |
-
//
|
| 761 |
const logoutBtn = document.querySelector('#logoutBtn');
|
| 762 |
if (logoutBtn) {
|
| 763 |
logoutBtn.addEventListener('click', async () => {
|
|
|
|
| 764 |
try {
|
| 765 |
const response = await fetch('/logout', {
|
| 766 |
method: 'POST',
|
|
@@ -768,26 +816,30 @@ if (logoutBtn) {
|
|
| 768 |
});
|
| 769 |
if (response.ok) {
|
| 770 |
localStorage.removeItem('token');
|
|
|
|
| 771 |
window.location.href = '/login';
|
| 772 |
} else {
|
|
|
|
| 773 |
alert('Failed to log out. Please try again.');
|
| 774 |
}
|
| 775 |
} catch (error) {
|
|
|
|
| 776 |
alert('Error during logout: ' + error.message);
|
| 777 |
}
|
| 778 |
});
|
| 779 |
}
|
| 780 |
|
| 781 |
-
//
|
| 782 |
if (uiElements.settingsBtn) {
|
| 783 |
uiElements.settingsBtn.addEventListener('click', async () => {
|
| 784 |
if (!checkAuth()) {
|
|
|
|
| 785 |
window.location.href = '/login';
|
| 786 |
return;
|
| 787 |
}
|
| 788 |
try {
|
| 789 |
const response = await fetch('/api/settings', {
|
| 790 |
-
headers: { 'Authorization': `Bearer ${
|
| 791 |
});
|
| 792 |
if (!response.ok) {
|
| 793 |
if (response.status === 401) {
|
|
@@ -826,6 +878,7 @@ if (uiElements.settingsBtn) {
|
|
| 826 |
uiElements.settingsModal.classList.remove('hidden');
|
| 827 |
toggleSidebar(false);
|
| 828 |
} catch (err) {
|
|
|
|
| 829 |
alert('Failed to load settings. Please try again.');
|
| 830 |
}
|
| 831 |
});
|
|
@@ -838,44 +891,42 @@ if (uiElements.closeSettingsBtn) {
|
|
| 838 |
}
|
| 839 |
|
| 840 |
if (uiElements.settingsForm) {
|
| 841 |
-
uiElements.settingsForm.addEventListener('submit', (e) => {
|
| 842 |
e.preventDefault();
|
| 843 |
if (!checkAuth()) {
|
|
|
|
| 844 |
window.location.href = '/login';
|
| 845 |
return;
|
| 846 |
}
|
| 847 |
const formData = new FormData(uiElements.settingsForm);
|
| 848 |
const data = Object.fromEntries(formData);
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
.then(res => {
|
| 858 |
-
if (!res.ok) {
|
| 859 |
-
if (res.status === 401) {
|
| 860 |
-
localStorage.removeItem('token');
|
| 861 |
-
window.location.href = '/login';
|
| 862 |
-
}
|
| 863 |
-
throw new Error('Failed to update settings');
|
| 864 |
-
}
|
| 865 |
-
return res.json();
|
| 866 |
-
})
|
| 867 |
-
.then(() => {
|
| 868 |
-
alert('Settings updated successfully!');
|
| 869 |
-
uiElements.settingsModal.classList.add('hidden');
|
| 870 |
-
toggleSidebar(false);
|
| 871 |
-
})
|
| 872 |
-
.catch(err => {
|
| 873 |
-
alert('Error updating settings: ' + err.message);
|
| 874 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
});
|
| 876 |
}
|
| 877 |
|
| 878 |
-
//
|
| 879 |
if (uiElements.historyToggle) {
|
| 880 |
uiElements.historyToggle.addEventListener('click', () => {
|
| 881 |
if (uiElements.conversationList) {
|
|
@@ -891,15 +942,15 @@ if (uiElements.historyToggle) {
|
|
| 891 |
});
|
| 892 |
}
|
| 893 |
|
| 894 |
-
//
|
| 895 |
uiElements.promptItems.forEach(p => {
|
| 896 |
p.addEventListener('click', e => {
|
| 897 |
e.preventDefault();
|
| 898 |
if (uiElements.input) {
|
| 899 |
uiElements.input.value = p.dataset.prompt;
|
| 900 |
autoResizeTextarea();
|
|
|
|
| 901 |
}
|
| 902 |
-
if (uiElements.sendBtn) uiElements.sendBtn.disabled = false;
|
| 903 |
submitMessage();
|
| 904 |
});
|
| 905 |
});
|
|
@@ -912,10 +963,11 @@ if (uiElements.audioInput) uiElements.audioInput.addEventListener('change', prev
|
|
| 912 |
if (uiElements.sendBtn) {
|
| 913 |
uiElements.sendBtn.addEventListener('click', (e) => {
|
| 914 |
e.preventDefault();
|
| 915 |
-
if (uiElements.sendBtn.disabled || isRequestActive || isRecording)
|
| 916 |
-
|
| 917 |
-
|
| 918 |
}
|
|
|
|
| 919 |
});
|
| 920 |
|
| 921 |
let pressTimer;
|
|
@@ -956,20 +1008,16 @@ if (uiElements.sendBtn) {
|
|
| 956 |
if (uiElements.form) {
|
| 957 |
uiElements.form.addEventListener('submit', (e) => {
|
| 958 |
e.preventDefault();
|
| 959 |
-
if (!isRecording &&
|
| 960 |
submitMessage();
|
| 961 |
}
|
| 962 |
});
|
| 963 |
}
|
| 964 |
|
| 965 |
if (uiElements.input) {
|
| 966 |
-
let debounceTimer;
|
| 967 |
uiElements.input.addEventListener('input', () => {
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
autoResizeTextarea();
|
| 971 |
-
updateSendButtonState();
|
| 972 |
-
}, 100);
|
| 973 |
});
|
| 974 |
uiElements.input.addEventListener('keydown', (e) => {
|
| 975 |
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -1006,7 +1054,14 @@ if (uiElements.newConversationBtn) {
|
|
| 1006 |
uiElements.newConversationBtn.addEventListener('click', createNewConversation);
|
| 1007 |
}
|
| 1008 |
|
| 1009 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1010 |
window.addEventListener('offline', () => {
|
| 1011 |
if (uiElements.messageLimitWarning) {
|
| 1012 |
uiElements.messageLimitWarning.classList.remove('hidden');
|
|
|
|
| 1 |
// SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
|
| 2 |
// SPDX-License-Identifier: Apache-2.0
|
| 3 |
|
| 4 |
+
// إعداد مكتبة Prism لتسليط الضوء على الكود
|
| 5 |
Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/';
|
| 6 |
|
| 7 |
+
// تعريف عناصر واجهة المستخدم
|
| 8 |
const uiElements = {
|
| 9 |
chatArea: document.getElementById('chatArea'),
|
| 10 |
chatBox: document.getElementById('chatBox'),
|
|
|
|
| 36 |
historyToggle: document.getElementById('historyToggle'),
|
| 37 |
};
|
| 38 |
|
| 39 |
+
// متغيرات الحالة
|
| 40 |
let conversationHistory = JSON.parse(sessionStorage.getItem('conversationHistory') || '[]');
|
| 41 |
let currentConversationId = window.conversationId || null;
|
| 42 |
let currentConversationTitle = window.conversationTitle || null;
|
|
|
|
| 48 |
let currentAssistantText = '';
|
| 49 |
let isSidebarOpen = window.innerWidth >= 768;
|
| 50 |
|
| 51 |
+
// تهيئة الصفحة
|
| 52 |
document.addEventListener('DOMContentLoaded', async () => {
|
| 53 |
AOS.init({
|
| 54 |
duration: 800,
|
|
|
|
| 57 |
offset: 50,
|
| 58 |
});
|
| 59 |
|
| 60 |
+
// التحقق من حالة تسجيل الدخول وتحميل المحادثة إن وجدت
|
| 61 |
if (currentConversationId && checkAuth()) {
|
| 62 |
+
console.log('Loading conversation with ID:', currentConversationId);
|
| 63 |
await loadConversation(currentConversationId);
|
| 64 |
} else if (conversationHistory.length > 0) {
|
| 65 |
+
console.log('Restoring conversation history from sessionStorage:', conversationHistory);
|
| 66 |
enterChatView();
|
| 67 |
conversationHistory.forEach(msg => {
|
| 68 |
+
console.log('Adding message from history:', msg);
|
| 69 |
addMsg(msg.role, msg.content);
|
| 70 |
});
|
| 71 |
+
} else {
|
| 72 |
+
console.log('No conversation history or ID, starting fresh');
|
| 73 |
}
|
| 74 |
|
| 75 |
autoResizeTextarea();
|
|
|
|
| 80 |
}, 3000);
|
| 81 |
}
|
| 82 |
setupTouchGestures();
|
|
|
|
| 83 |
});
|
| 84 |
|
| 85 |
+
// التحقق من رمز التوثيق
|
| 86 |
function checkAuth() {
|
| 87 |
const token = localStorage.getItem('token');
|
| 88 |
+
console.log('Auth token:', token ? 'Found' : 'Not found');
|
| 89 |
+
return token;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
+
// إدارة الجلسة لغير المسجلين
|
| 93 |
async function handleSession() {
|
| 94 |
const sessionId = sessionStorage.getItem('session_id');
|
| 95 |
if (!sessionId) {
|
| 96 |
const newSessionId = crypto.randomUUID();
|
| 97 |
sessionStorage.setItem('session_id', newSessionId);
|
| 98 |
+
console.log('New session_id created:', newSessionId);
|
| 99 |
return newSessionId;
|
| 100 |
}
|
| 101 |
+
console.log('Existing session_id:', sessionId);
|
| 102 |
return sessionId;
|
| 103 |
}
|
| 104 |
|
| 105 |
+
// تحديث حالة زر الإرسال
|
| 106 |
function updateSendButtonState() {
|
| 107 |
if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) {
|
| 108 |
+
const hasInput = uiElements.input.value.trim() !== '' ||
|
| 109 |
+
uiElements.fileInput.files.length > 0 ||
|
| 110 |
+
uiElements.audioInput.files.length > 0;
|
| 111 |
+
uiElements.sendBtn.disabled = !hasInput || isRequestActive || isRecording;
|
| 112 |
+
console.log('Send button state:', uiElements.sendBtn.disabled ? 'Disabled' : 'Enabled', 'Input:', uiElements.input.value, 'Files:', uiElements.fileInput.files.length, 'Audio:', uiElements.audioInput.files.length);
|
| 113 |
}
|
| 114 |
}
|
| 115 |
|
| 116 |
+
// عرض محتوى Markdown مع دعم RTL
|
| 117 |
function renderMarkdown(el) {
|
| 118 |
const raw = el.dataset.text || '';
|
| 119 |
const isArabic = isArabicText(raw);
|
|
|
|
| 144 |
}
|
| 145 |
}
|
| 146 |
|
| 147 |
+
// الانتقال إلى عرض المحادثة
|
| 148 |
function enterChatView() {
|
| 149 |
if (uiElements.chatHeader) {
|
| 150 |
uiElements.chatHeader.classList.remove('hidden');
|
|
|
|
| 157 |
if (uiElements.initialContent) uiElements.initialContent.classList.add('hidden');
|
| 158 |
}
|
| 159 |
|
| 160 |
+
// العودة إلى العرض الافتراضي
|
| 161 |
function leaveChatView() {
|
| 162 |
if (uiElements.chatHeader) {
|
| 163 |
uiElements.chatHeader.classList.add('hidden');
|
|
|
|
| 167 |
if (uiElements.initialContent) uiElements.initialContent.classList.remove('hidden');
|
| 168 |
}
|
| 169 |
|
| 170 |
+
// إضافة رسالة إلى واجهة المستخدم
|
| 171 |
function addMsg(who, text) {
|
| 172 |
const div = document.createElement('div');
|
| 173 |
div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`;
|
| 174 |
div.dataset.text = text;
|
| 175 |
+
console.log('Adding message:', { who, text });
|
| 176 |
renderMarkdown(div);
|
| 177 |
if (uiElements.chatBox) {
|
| 178 |
uiElements.chatBox.appendChild(div);
|
| 179 |
uiElements.chatBox.classList.remove('hidden');
|
| 180 |
+
} else {
|
| 181 |
+
console.error('chatBox is null');
|
| 182 |
}
|
| 183 |
return div;
|
| 184 |
}
|
| 185 |
|
| 186 |
+
// مسح جميع الرسائل
|
| 187 |
function clearAllMessages() {
|
| 188 |
stopStream(true);
|
| 189 |
conversationHistory = [];
|
|
|
|
| 208 |
autoResizeTextarea();
|
| 209 |
}
|
| 210 |
|
| 211 |
+
// معاينة الملفات
|
| 212 |
function previewFile() {
|
| 213 |
if (uiElements.fileInput?.files.length > 0) {
|
| 214 |
const file = uiElements.fileInput.files[0];
|
|
|
|
| 242 |
}
|
| 243 |
}
|
| 244 |
|
| 245 |
+
// تسجيل الصوت
|
| 246 |
function startVoiceRecording() {
|
| 247 |
+
if (isRequestActive || isRecording) {
|
| 248 |
+
console.log('Voice recording blocked: Request active or already recording');
|
| 249 |
+
return;
|
| 250 |
+
}
|
| 251 |
+
console.log('Starting voice recording...');
|
| 252 |
isRecording = true;
|
| 253 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.add('recording');
|
| 254 |
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
|
| 255 |
mediaRecorder = new MediaRecorder(stream);
|
| 256 |
audioChunks = [];
|
| 257 |
mediaRecorder.start();
|
| 258 |
+
console.log('MediaRecorder started');
|
| 259 |
mediaRecorder.addEventListener('dataavailable', event => {
|
| 260 |
audioChunks.push(event.data);
|
| 261 |
+
console.log('Audio chunk received:', event.data);
|
| 262 |
});
|
| 263 |
}).catch(err => {
|
| 264 |
+
console.error('Error accessing microphone:', err);
|
| 265 |
alert('Failed to access microphone. Please check permissions.');
|
| 266 |
isRecording = false;
|
| 267 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
|
|
|
|
| 274 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
|
| 275 |
isRecording = false;
|
| 276 |
mediaRecorder.addEventListener('stop', async () => {
|
| 277 |
+
console.log('Stopping voice recording, sending audio...');
|
| 278 |
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
| 279 |
const formData = new FormData();
|
| 280 |
formData.append('file', audioBlob, 'voice-message.webm');
|
|
|
|
| 283 |
}
|
| 284 |
}
|
| 285 |
|
| 286 |
+
// إرسال رسالة صوتية
|
| 287 |
async function submitAudioMessage(formData) {
|
| 288 |
enterChatView();
|
| 289 |
addMsg('user', 'Voice message');
|
|
|
|
| 329 |
}
|
| 330 |
}
|
| 331 |
|
| 332 |
+
// إرسال طلب إلى الخادم
|
| 333 |
async function sendRequest(endpoint, body, headers = {}) {
|
| 334 |
+
const token = checkAuth();
|
| 335 |
if (token) headers['Authorization'] = `Bearer ${token}`;
|
| 336 |
headers['X-Session-ID'] = await handleSession();
|
| 337 |
+
console.log('Sending request to:', endpoint, 'with headers:', headers);
|
| 338 |
try {
|
| 339 |
const response = await fetch(endpoint, {
|
| 340 |
method: 'POST',
|
|
|
|
| 359 |
}
|
| 360 |
return response;
|
| 361 |
} catch (error) {
|
| 362 |
+
console.error('Send request error:', error);
|
| 363 |
if (error.name === 'AbortError') {
|
| 364 |
throw new Error('Request was aborted');
|
| 365 |
}
|
|
|
|
| 367 |
}
|
| 368 |
}
|
| 369 |
|
| 370 |
+
// تحديث واجهة المستخدم أثناء الطلب
|
| 371 |
function updateUIForRequest() {
|
| 372 |
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'inline-flex';
|
| 373 |
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'none';
|
|
|
|
| 378 |
autoResizeTextarea();
|
| 379 |
}
|
| 380 |
|
| 381 |
+
// إنهاء الطلب
|
| 382 |
function finalizeRequest() {
|
| 383 |
streamMsg = null;
|
| 384 |
isRequestActive = false;
|
|
|
|
| 387 |
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
|
| 388 |
}
|
| 389 |
|
| 390 |
+
// معالجة أخطاء الطلب
|
| 391 |
function handleRequestError(error) {
|
| 392 |
if (streamMsg) {
|
| 393 |
streamMsg.querySelector('.loading')?.remove();
|
|
|
|
| 401 |
streamMsg.dataset.done = '1';
|
| 402 |
streamMsg = null;
|
| 403 |
}
|
| 404 |
+
console.error('Request error:', error);
|
| 405 |
alert(`Error: ${error.message || 'An error occurred during the request.'}`);
|
| 406 |
isRequestActive = false;
|
| 407 |
abortController = null;
|
|
|
|
| 410 |
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
|
| 411 |
}
|
| 412 |
|
| 413 |
+
// تحميل المحادثات للشريط الجانبي
|
| 414 |
async function loadConversations() {
|
| 415 |
if (!checkAuth()) return;
|
| 416 |
try {
|
| 417 |
const response = await fetch('/api/conversations', {
|
| 418 |
+
headers: { 'Authorization': `Bearer ${checkAuth()}` }
|
| 419 |
});
|
| 420 |
if (!response.ok) throw new Error('Failed to load conversations');
|
| 421 |
const conversations = await response.json();
|
|
|
|
| 444 |
});
|
| 445 |
}
|
| 446 |
} catch (error) {
|
| 447 |
+
console.error('Error loading conversations:', error);
|
| 448 |
alert('Failed to load conversations. Please try again.');
|
| 449 |
}
|
| 450 |
}
|
| 451 |
|
| 452 |
+
// تحميل محادثة من الخادم
|
| 453 |
async function loadConversation(conversationId) {
|
| 454 |
try {
|
| 455 |
const response = await fetch(`/api/conversations/${conversationId}`, {
|
| 456 |
+
headers: { 'Authorization': `Bearer ${checkAuth()}` }
|
| 457 |
});
|
| 458 |
if (!response.ok) {
|
| 459 |
+
if (response.status === 401) {
|
| 460 |
+
localStorage.removeItem('token');
|
| 461 |
+
window.location.href = '/login';
|
| 462 |
+
}
|
| 463 |
throw new Error('Failed to load conversation');
|
| 464 |
}
|
| 465 |
const data = await response.json();
|
|
|
|
| 473 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 474 |
toggleSidebar(false);
|
| 475 |
} catch (error) {
|
| 476 |
+
console.error('Error loading conversation:', error);
|
| 477 |
alert('Failed to load conversation. Please try again or log in.');
|
| 478 |
}
|
| 479 |
}
|
| 480 |
|
| 481 |
+
// حذف محادثة
|
| 482 |
async function deleteConversation(conversationId) {
|
| 483 |
if (!confirm('Are you sure you want to delete this conversation?')) return;
|
| 484 |
try {
|
| 485 |
const response = await fetch(`/api/conversations/${conversationId}`, {
|
| 486 |
method: 'DELETE',
|
| 487 |
+
headers: { 'Authorization': `Bearer ${checkAuth()}` }
|
| 488 |
});
|
| 489 |
if (!response.ok) {
|
| 490 |
+
if (response.status === 401) {
|
| 491 |
+
localStorage.removeItem('token');
|
| 492 |
+
window.location.href = '/login';
|
| 493 |
+
}
|
| 494 |
throw new Error('Failed to delete conversation');
|
| 495 |
}
|
| 496 |
if (conversationId === currentConversationId) {
|
|
|
|
| 501 |
}
|
| 502 |
await loadConversations();
|
| 503 |
} catch (error) {
|
| 504 |
+
console.error('Error deleting conversation:', error);
|
| 505 |
alert('Failed to delete conversation. Please try again.');
|
| 506 |
}
|
| 507 |
}
|
| 508 |
|
| 509 |
+
// حفظ رسالة في المحادثة
|
| 510 |
async function saveMessageToConversation(conversationId, role, content) {
|
| 511 |
try {
|
| 512 |
const response = await fetch(`/api/conversations/${conversationId}`, {
|
| 513 |
method: 'POST',
|
| 514 |
headers: {
|
| 515 |
'Content-Type': 'application/json',
|
| 516 |
+
'Authorization': `Bearer ${checkAuth()}`
|
| 517 |
},
|
| 518 |
body: JSON.stringify({ role, content })
|
| 519 |
});
|
| 520 |
+
if (!response.ok) {
|
| 521 |
+
if (response.status === 401) {
|
| 522 |
+
localStorage.removeItem('token');
|
| 523 |
+
window.location.href = '/login';
|
| 524 |
+
}
|
| 525 |
+
throw new Error('Failed to save message');
|
| 526 |
+
}
|
| 527 |
} catch (error) {
|
| 528 |
console.error('Error saving message:', error);
|
| 529 |
}
|
| 530 |
}
|
| 531 |
|
| 532 |
+
// إنشاء محادثة جديدة
|
| 533 |
async function createNewConversation() {
|
| 534 |
if (!checkAuth()) {
|
| 535 |
+
alert('Please log in to create a new conversation.');
|
| 536 |
window.location.href = '/login';
|
| 537 |
return;
|
| 538 |
}
|
|
|
|
| 541 |
method: 'POST',
|
| 542 |
headers: {
|
| 543 |
'Content-Type': 'application/json',
|
| 544 |
+
'Authorization': `Bearer ${checkAuth()}`
|
| 545 |
},
|
| 546 |
body: JSON.stringify({ title: 'New Conversation' })
|
| 547 |
});
|
|
|
|
| 564 |
await loadConversations();
|
| 565 |
toggleSidebar(false);
|
| 566 |
} catch (error) {
|
| 567 |
+
console.error('Error creating conversation:', error);
|
| 568 |
alert('Failed to create new conversation. Please try again.');
|
| 569 |
}
|
| 570 |
if (uiElements.chatBox) uiElements.chatBox.scrollTo({
|
|
|
|
| 573 |
});
|
| 574 |
}
|
| 575 |
|
| 576 |
+
// تحديث عنوان المحادثة
|
| 577 |
async function updateConversationTitle(conversationId, newTitle) {
|
| 578 |
try {
|
| 579 |
const response = await fetch(`/api/conversations/${conversationId}/title`, {
|
| 580 |
method: 'PUT',
|
| 581 |
headers: {
|
| 582 |
'Content-Type': 'application/json',
|
| 583 |
+
'Authorization': `Bearer ${checkAuth()}`
|
| 584 |
},
|
| 585 |
body: JSON.stringify({ title: newTitle })
|
| 586 |
});
|
| 587 |
+
if (!response.ok) {
|
| 588 |
+
if (response.status === 401) {
|
| 589 |
+
localStorage.removeItem('token');
|
| 590 |
+
window.location.href = '/login';
|
| 591 |
+
}
|
| 592 |
+
throw new Error('Failed to update title');
|
| 593 |
+
}
|
| 594 |
const data = await response.json();
|
| 595 |
currentConversationTitle = data.title;
|
| 596 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 597 |
await loadConversations();
|
| 598 |
} catch (error) {
|
| 599 |
+
console.error('Error updating title:', error);
|
| 600 |
alert('Failed to update conversation title.');
|
| 601 |
}
|
| 602 |
}
|
| 603 |
|
| 604 |
+
// إعداد إيماءات اللمس
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
function setupTouchGestures() {
|
| 606 |
if (!uiElements.sidebar) return;
|
| 607 |
+
const hammer = new Hammer(uiElements.sidebar);
|
| 608 |
const mainContent = document.querySelector('.flex-1');
|
| 609 |
const hammerMain = new Hammer(mainContent);
|
| 610 |
|
| 611 |
+
hammer.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL });
|
| 612 |
hammer.on('pan', e => {
|
| 613 |
if (!isSidebarOpen) return;
|
| 614 |
let translateX = Math.max(-uiElements.sidebar.offsetWidth, Math.min(0, e.deltaX));
|
| 615 |
uiElements.sidebar.style.transform = `translateX(${translateX}px)`;
|
| 616 |
+
uiElements.sidebar.style.transition = 'none';
|
| 617 |
});
|
| 618 |
hammer.on('panend', e => {
|
| 619 |
+
uiElements.sidebar.style.transition = 'transform 0.3s ease-in-out';
|
| 620 |
if (e.deltaX < -50) {
|
| 621 |
toggleSidebar(false);
|
| 622 |
} else {
|
|
|
|
| 624 |
}
|
| 625 |
});
|
| 626 |
|
| 627 |
+
hammerMain.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL });
|
| 628 |
+
hammerMain.on('panstart', e => {
|
| 629 |
+
if (isSidebarOpen) return;
|
| 630 |
+
if (e.center.x < 50 || e.center.x > window.innerWidth - 50) {
|
| 631 |
+
uiElements.sidebar.style.transition = 'none';
|
| 632 |
+
}
|
| 633 |
+
});
|
| 634 |
hammerMain.on('pan', e => {
|
| 635 |
if (isSidebarOpen) return;
|
| 636 |
+
if (e.center.x < 50 || e.center.x > window.innerWidth - 50) {
|
| 637 |
+
let translateX = e.center.x < 50
|
| 638 |
+
? Math.min(uiElements.sidebar.offsetWidth, Math.max(0, e.deltaX))
|
| 639 |
+
: Math.max(-uiElements.sidebar.offsetWidth, Math.min(0, e.deltaX));
|
| 640 |
+
uiElements.sidebar.style.transform = `translateX(${translateX - uiElements.sidebar.offsetWidth}px)`;
|
| 641 |
+
}
|
| 642 |
});
|
| 643 |
hammerMain.on('panend', e => {
|
| 644 |
+
uiElements.sidebar.style.transition = 'transform 0.3s ease-in-out';
|
| 645 |
+
if (e.center.x < 50 && e.deltaX > 50) {
|
| 646 |
+
toggleSidebar(true);
|
| 647 |
+
} else if (e.center.x > window.innerWidth - 50 && e.deltaX < -50) {
|
| 648 |
toggleSidebar(true);
|
| 649 |
} else {
|
| 650 |
toggleSidebar(false);
|
|
|
|
| 652 |
});
|
| 653 |
}
|
| 654 |
|
| 655 |
+
// إرسال رسالة المستخدم
|
| 656 |
async function submitMessage() {
|
| 657 |
+
if (isRequestActive || isRecording) {
|
| 658 |
+
console.log('Submit blocked: Request active or recording');
|
| 659 |
+
return;
|
| 660 |
+
}
|
| 661 |
let message = uiElements.input?.value.trim() || '';
|
| 662 |
let payload = null;
|
| 663 |
let formData = null;
|
| 664 |
let endpoint = '/api/chat';
|
| 665 |
let headers = {};
|
| 666 |
+
let inputType = 'text';
|
| 667 |
+
let outputFormat = 'text';
|
| 668 |
+
let title = null;
|
| 669 |
|
| 670 |
+
if (!message && !uiElements.fileInput?.files.length && !uiElements.audioInput?.files.length) {
|
| 671 |
+
console.log('No message, file, or audio to send');
|
| 672 |
+
return;
|
| 673 |
+
}
|
| 674 |
|
| 675 |
if (uiElements.fileInput?.files.length > 0) {
|
| 676 |
const file = uiElements.fileInput.files[0];
|
| 677 |
if (file.type.startsWith('image/')) {
|
| 678 |
endpoint = '/api/image-analysis';
|
| 679 |
+
inputType = 'image';
|
| 680 |
message = 'Analyze this image';
|
| 681 |
formData = new FormData();
|
| 682 |
formData.append('file', file);
|
|
|
|
| 686 |
const file = uiElements.audioInput.files[0];
|
| 687 |
if (file.type.startsWith('audio/')) {
|
| 688 |
endpoint = '/api/audio-transcription';
|
| 689 |
+
inputType = 'audio';
|
| 690 |
message = 'Transcribe this audio';
|
| 691 |
formData = new FormData();
|
| 692 |
formData.append('file', file);
|
|
|
|
| 701 |
temperature: 0.7,
|
| 702 |
max_new_tokens: 128000,
|
| 703 |
enable_browsing: true,
|
| 704 |
+
output_format: 'text',
|
| 705 |
+
title: title
|
| 706 |
};
|
| 707 |
headers['Content-Type'] = 'application/json';
|
| 708 |
}
|
|
|
|
| 725 |
let responseText = '';
|
| 726 |
if (endpoint === '/api/audio-transcription') {
|
| 727 |
const data = await response.json();
|
| 728 |
+
if (!data.transcription) throw new Error('No transcription received from server');
|
| 729 |
responseText = data.transcription || 'Error: No transcription generated.';
|
| 730 |
} else if (endpoint === '/api/image-analysis') {
|
| 731 |
const data = await response.json();
|
|
|
|
| 748 |
let buffer = '';
|
| 749 |
while (true) {
|
| 750 |
const { done, value } = await reader.read();
|
| 751 |
+
if (done) {
|
| 752 |
+
if (!buffer.trim()) throw new Error('Empty response from server');
|
| 753 |
+
break;
|
| 754 |
+
}
|
| 755 |
buffer += decoder.decode(value, { stream: true });
|
| 756 |
if (streamMsg) {
|
| 757 |
streamMsg.dataset.text = buffer;
|
|
|
|
| 783 |
}
|
| 784 |
}
|
| 785 |
|
| 786 |
+
// إيقاف البث
|
| 787 |
function stopStream(forceCancel = false) {
|
| 788 |
if (!isRequestActive && !isRecording) return;
|
| 789 |
if (isRecording) stopVoiceRecording();
|
|
|
|
| 804 |
if (uiElements.stopBtn) uiElements.stopBtn.style.pointerEvents = 'auto';
|
| 805 |
}
|
| 806 |
|
| 807 |
+
// معالجة تسجيل الخروج
|
| 808 |
const logoutBtn = document.querySelector('#logoutBtn');
|
| 809 |
if (logoutBtn) {
|
| 810 |
logoutBtn.addEventListener('click', async () => {
|
| 811 |
+
console.log('Logout button clicked');
|
| 812 |
try {
|
| 813 |
const response = await fetch('/logout', {
|
| 814 |
method: 'POST',
|
|
|
|
| 816 |
});
|
| 817 |
if (response.ok) {
|
| 818 |
localStorage.removeItem('token');
|
| 819 |
+
console.log('Token removed from localStorage');
|
| 820 |
window.location.href = '/login';
|
| 821 |
} else {
|
| 822 |
+
console.error('Logout failed:', response.status);
|
| 823 |
alert('Failed to log out. Please try again.');
|
| 824 |
}
|
| 825 |
} catch (error) {
|
| 826 |
+
console.error('Logout error:', error);
|
| 827 |
alert('Error during logout: ' + error.message);
|
| 828 |
}
|
| 829 |
});
|
| 830 |
}
|
| 831 |
|
| 832 |
+
// إعدادات المستخدم
|
| 833 |
if (uiElements.settingsBtn) {
|
| 834 |
uiElements.settingsBtn.addEventListener('click', async () => {
|
| 835 |
if (!checkAuth()) {
|
| 836 |
+
alert('Please log in to access settings.');
|
| 837 |
window.location.href = '/login';
|
| 838 |
return;
|
| 839 |
}
|
| 840 |
try {
|
| 841 |
const response = await fetch('/api/settings', {
|
| 842 |
+
headers: { 'Authorization': `Bearer ${checkAuth()}` }
|
| 843 |
});
|
| 844 |
if (!response.ok) {
|
| 845 |
if (response.status === 401) {
|
|
|
|
| 878 |
uiElements.settingsModal.classList.remove('hidden');
|
| 879 |
toggleSidebar(false);
|
| 880 |
} catch (err) {
|
| 881 |
+
console.error('Error fetching settings:', err);
|
| 882 |
alert('Failed to load settings. Please try again.');
|
| 883 |
}
|
| 884 |
});
|
|
|
|
| 891 |
}
|
| 892 |
|
| 893 |
if (uiElements.settingsForm) {
|
| 894 |
+
uiElements.settingsForm.addEventListener('submit', async (e) => {
|
| 895 |
e.preventDefault();
|
| 896 |
if (!checkAuth()) {
|
| 897 |
+
alert('Please log in to save settings.');
|
| 898 |
window.location.href = '/login';
|
| 899 |
return;
|
| 900 |
}
|
| 901 |
const formData = new FormData(uiElements.settingsForm);
|
| 902 |
const data = Object.fromEntries(formData);
|
| 903 |
+
try {
|
| 904 |
+
const response = await fetch('/users/me', {
|
| 905 |
+
method: 'PUT',
|
| 906 |
+
headers: {
|
| 907 |
+
'Content-Type': 'application/json',
|
| 908 |
+
'Authorization': `Bearer ${checkAuth()}`
|
| 909 |
+
},
|
| 910 |
+
body: JSON.stringify(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
});
|
| 912 |
+
if (!response.ok) {
|
| 913 |
+
if (response.status === 401) {
|
| 914 |
+
localStorage.removeItem('token');
|
| 915 |
+
window.location.href = '/login';
|
| 916 |
+
}
|
| 917 |
+
throw new Error('Failed to update settings');
|
| 918 |
+
}
|
| 919 |
+
alert('Settings updated successfully!');
|
| 920 |
+
uiElements.settingsModal.classList.add('hidden');
|
| 921 |
+
toggleSidebar(false);
|
| 922 |
+
} catch (err) {
|
| 923 |
+
console.error('Error updating settings:', err);
|
| 924 |
+
alert('Error updating settings: ' + err.message);
|
| 925 |
+
}
|
| 926 |
});
|
| 927 |
}
|
| 928 |
|
| 929 |
+
// تبديل عرض السجل
|
| 930 |
if (uiElements.historyToggle) {
|
| 931 |
uiElements.historyToggle.addEventListener('click', () => {
|
| 932 |
if (uiElements.conversationList) {
|
|
|
|
| 942 |
});
|
| 943 |
}
|
| 944 |
|
| 945 |
+
// إضافة مستمعي الأحداث
|
| 946 |
uiElements.promptItems.forEach(p => {
|
| 947 |
p.addEventListener('click', e => {
|
| 948 |
e.preventDefault();
|
| 949 |
if (uiElements.input) {
|
| 950 |
uiElements.input.value = p.dataset.prompt;
|
| 951 |
autoResizeTextarea();
|
| 952 |
+
updateSendButtonState();
|
| 953 |
}
|
|
|
|
| 954 |
submitMessage();
|
| 955 |
});
|
| 956 |
});
|
|
|
|
| 963 |
if (uiElements.sendBtn) {
|
| 964 |
uiElements.sendBtn.addEventListener('click', (e) => {
|
| 965 |
e.preventDefault();
|
| 966 |
+
if (uiElements.sendBtn.disabled || isRequestActive || isRecording) {
|
| 967 |
+
console.log('Send button click ignored: disabled or request/recording active');
|
| 968 |
+
return;
|
| 969 |
}
|
| 970 |
+
submitMessage();
|
| 971 |
});
|
| 972 |
|
| 973 |
let pressTimer;
|
|
|
|
| 1008 |
if (uiElements.form) {
|
| 1009 |
uiElements.form.addEventListener('submit', (e) => {
|
| 1010 |
e.preventDefault();
|
| 1011 |
+
if (!isRecording && !uiElements.sendBtn.disabled) {
|
| 1012 |
submitMessage();
|
| 1013 |
}
|
| 1014 |
});
|
| 1015 |
}
|
| 1016 |
|
| 1017 |
if (uiElements.input) {
|
|
|
|
| 1018 |
uiElements.input.addEventListener('input', () => {
|
| 1019 |
+
autoResizeTextarea();
|
| 1020 |
+
updateSendButtonState();
|
|
|
|
|
|
|
|
|
|
| 1021 |
});
|
| 1022 |
uiElements.input.addEventListener('keydown', (e) => {
|
| 1023 |
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
| 1054 |
uiElements.newConversationBtn.addEventListener('click', createNewConversation);
|
| 1055 |
}
|
| 1056 |
|
| 1057 |
+
// إزالة التكرارات في localStorage
|
| 1058 |
+
const originalRemoveItem = localStorage.removeItem;
|
| 1059 |
+
localStorage.removeItem = function (key) {
|
| 1060 |
+
console.log('Removing from localStorage:', key);
|
| 1061 |
+
originalRemoveItem.apply(this, arguments);
|
| 1062 |
+
};
|
| 1063 |
+
|
| 1064 |
+
// التعامل مع وضع عدم الاتصال
|
| 1065 |
window.addEventListener('offline', () => {
|
| 1066 |
if (uiElements.messageLimitWarning) {
|
| 1067 |
uiElements.messageLimitWarning.classList.remove('hidden');
|