Mark-Lasfar commited on
Commit
72a556f
·
1 Parent(s): 596833b
Files changed (1) hide show
  1. 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 for code highlighting
5
  Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/';
6
 
7
- // UI elements
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
- // State variables
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
- // Initialize AOS and load initial conversation
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
- // Check authentication token with additional validation
81
  function checkAuth() {
82
  const token = localStorage.getItem('token');
83
- if (token) {
84
- // Verify token validity by making a quick API call
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
- // Handle session for non-logged-in users
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
- // Update send button state with forced enable if input has content
113
  function updateSendButtonState() {
114
  if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) {
115
- const hasInput = uiElements.input.value.trim() !== '' ||
116
- uiElements.fileInput.files.length > 0 ||
117
- uiElements.audioInput.files.length > 0;
118
- uiElements.sendBtn.disabled = !hasInput;
 
119
  }
120
  }
121
 
122
- // Render markdown content with RTL support
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
- // Toggle chat view
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
- // Toggle home view
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
- // Add chat bubble
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
- // Clear all messages
190
  function clearAllMessages() {
191
  stopStream(true);
192
  conversationHistory = [];
@@ -211,7 +208,7 @@ function clearAllMessages() {
211
  autoResizeTextarea();
212
  }
213
 
214
- // File preview
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
- // Voice recording
249
  function startVoiceRecording() {
250
- if (isRequestActive || isRecording) return;
 
 
 
 
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
- // Send audio message
282
  async function submitAudioMessage(formData) {
283
  enterChatView();
284
  addMsg('user', 'Voice message');
@@ -324,11 +329,12 @@ async function submitAudioMessage(formData) {
324
  }
325
  }
326
 
327
- // Helper to send API requests
328
  async function sendRequest(endpoint, body, headers = {}) {
329
- const token = localStorage.getItem('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
- // Helper to update UI during request
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
- // Helper to finalize request
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
- // Helper to handle request errors
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
- // Load conversations for sidebar
406
  async function loadConversations() {
407
  if (!checkAuth()) return;
408
  try {
409
  const response = await fetch('/api/conversations', {
410
- headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
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
- // Load conversation from API
444
  async function loadConversation(conversationId) {
445
  try {
446
  const response = await fetch(`/api/conversations/${conversationId}`, {
447
- headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
448
  });
449
  if (!response.ok) {
450
- if (response.status === 401) window.location.href = '/login';
 
 
 
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
- // Delete conversation
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 ${localStorage.getItem('token')}` }
475
  });
476
  if (!response.ok) {
477
- if (response.status === 401) window.location.href = '/login';
 
 
 
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
- // Save message to conversation
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 ${localStorage.getItem('token')}`
500
  },
501
  body: JSON.stringify({ role, content })
502
  });
503
- if (!response.ok) throw new Error('Failed to save message');
 
 
 
 
 
 
504
  } catch (error) {
505
  console.error('Error saving message:', error);
506
  }
507
  }
508
 
509
- // Create new conversation with forced auth check
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 ${localStorage.getItem('token')}`
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
- // Update conversation title
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 ${localStorage.getItem('token')}`
559
  },
560
  body: JSON.stringify({ title: newTitle })
561
  });
562
- if (!response.ok) throw new Error('Failed to update title');
 
 
 
 
 
 
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
- // Toggle sidebar with optimized performance
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.parentElement); // Attach to parent for better touch detection
592
  const mainContent = document.querySelector('.flex-1');
593
  const hammerMain = new Hammer(mainContent);
594
 
595
- hammer.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL, threshold: 0 }); // Reduce threshold for faster response
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, threshold: 0 });
 
 
 
 
 
 
610
  hammerMain.on('pan', e => {
611
  if (isSidebarOpen) return;
612
- let translateX = Math.min(uiElements.sidebar.offsetWidth, Math.max(0, e.deltaX));
613
- uiElements.sidebar.style.transform = `translateX(${translateX - uiElements.sidebar.offsetWidth}px)`;
 
 
 
 
614
  });
615
  hammerMain.on('panend', e => {
616
- if (e.deltaX > 50) {
 
 
 
617
  toggleSidebar(true);
618
  } else {
619
  toggleSidebar(false);
@@ -621,21 +652,31 @@ function setupTouchGestures() {
621
  });
622
  }
623
 
624
- // Send user message with reduced checks
625
  async function submitMessage() {
626
- if (isRequestActive || isRecording) return;
 
 
 
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) return;
 
 
 
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) break;
 
 
 
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
- // Stop streaming
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
- // Logout handler
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
- // Settings Modal with forced auth
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 ${localStorage.getItem('token')}` }
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
- fetch('/users/me', {
850
- method: 'PUT',
851
- headers: {
852
- 'Content-Type': 'application/json',
853
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
854
- },
855
- body: JSON.stringify(data)
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
- // History Toggle
879
  if (uiElements.historyToggle) {
880
  uiElements.historyToggle.addEventListener('click', () => {
881
  if (uiElements.conversationList) {
@@ -891,15 +942,15 @@ if (uiElements.historyToggle) {
891
  });
892
  }
893
 
894
- // Event listeners
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) return;
916
- if (uiElements.input.value.trim() || uiElements.fileInput.files.length || uiElements.audioInput.files.length) {
917
- submitMessage();
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 && (uiElements.input.value.trim() || uiElements.fileInput.files.length || uiElements.audioInput.files.length)) {
960
  submitMessage();
961
  }
962
  });
963
  }
964
 
965
  if (uiElements.input) {
966
- let debounceTimer;
967
  uiElements.input.addEventListener('input', () => {
968
- clearTimeout(debounceTimer);
969
- debounceTimer = setTimeout(() => {
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
- // Offline mode detection
 
 
 
 
 
 
 
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');