Mark-Lasfar commited on
Commit
f46462b
·
1 Parent(s): 72a556f
Files changed (2) hide show
  1. api/endpoints.py +8 -0
  2. static/js/chat.js +178 -180
api/endpoints.py CHANGED
@@ -817,6 +817,14 @@ async def get_user_settings(user: User = Depends(current_active_user)):
817
  "is_superuser": user.is_superuser
818
  }
819
 
 
 
 
 
 
 
 
 
820
  @router.put("/users/me")
821
  async def update_user_settings(
822
  settings: UserUpdate,
 
817
  "is_superuser": user.is_superuser
818
  }
819
 
820
+
821
+ @router.get("/api/verify-token")
822
+ async def verify_token(user: User = Depends(current_active_user)):
823
+ if not user:
824
+ raise HTTPException(status_code=401, detail="Invalid or expired token")
825
+ return {"status": "valid"}
826
+
827
+
828
  @router.put("/users/me")
829
  async def update_user_settings(
830
  settings: UserUpdate,
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,11 +57,10 @@ document.addEventListener('DOMContentLoaded', async () => {
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 => {
@@ -82,14 +81,33 @@ document.addEventListener('DOMContentLoaded', async () => {
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) {
@@ -102,18 +120,18 @@ async function handleSession() {
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,7 +162,7 @@ function renderMarkdown(el) {
144
  }
145
  }
146
 
147
- // الانتقال إلى عرض المحادثة
148
  function enterChatView() {
149
  if (uiElements.chatHeader) {
150
  uiElements.chatHeader.classList.remove('hidden');
@@ -157,7 +175,7 @@ function enterChatView() {
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,7 +185,7 @@ function leaveChatView() {
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' : ''}`;
@@ -183,11 +201,11 @@ function addMsg(who, text) {
183
  return div;
184
  }
185
 
186
- // مسح جميع الرسائل
187
  function clearAllMessages() {
188
  stopStream(true);
189
  conversationHistory = [];
190
- sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
191
  currentAssistantText = '';
192
  if (streamMsg) {
193
  streamMsg.querySelector('.loading')?.remove();
@@ -208,7 +226,7 @@ function clearAllMessages() {
208
  autoResizeTextarea();
209
  }
210
 
211
- // معاينة الملفات
212
  function previewFile() {
213
  if (uiElements.fileInput?.files.length > 0) {
214
  const file = uiElements.fileInput.files[0];
@@ -242,7 +260,7 @@ function previewFile() {
242
  }
243
  }
244
 
245
- // تسجيل الصوت
246
  function startVoiceRecording() {
247
  if (isRequestActive || isRecording) {
248
  console.log('Voice recording blocked: Request active or already recording');
@@ -283,12 +301,14 @@ function stopVoiceRecording() {
283
  }
284
  }
285
 
286
- // إرسال رسالة صوتية
287
  async function submitAudioMessage(formData) {
288
  enterChatView();
289
  addMsg('user', 'Voice message');
290
- conversationHistory.push({ role: 'user', content: 'Voice message' });
291
- sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
 
 
292
  streamMsg = addMsg('assistant', '');
293
  const loadingEl = document.createElement('span');
294
  loadingEl.className = 'loading';
@@ -311,10 +331,9 @@ async function submitAudioMessage(formData) {
311
  renderMarkdown(streamMsg);
312
  streamMsg.dataset.done = '1';
313
  }
314
- conversationHistory.push({ role: 'assistant', content: transcription });
315
- sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
316
- if (checkAuth() && currentConversationId) {
317
- await saveMessageToConversation(currentConversationId, 'assistant', transcription);
318
  }
319
  if (checkAuth() && data.conversation_id) {
320
  currentConversationId = data.conversation_id;
@@ -329,9 +348,9 @@ async function submitAudioMessage(formData) {
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);
@@ -367,7 +386,7 @@ async function sendRequest(endpoint, body, headers = {}) {
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,7 +397,7 @@ function updateUIForRequest() {
378
  autoResizeTextarea();
379
  }
380
 
381
- // إنهاء الطلب
382
  function finalizeRequest() {
383
  streamMsg = null;
384
  isRequestActive = false;
@@ -387,7 +406,7 @@ function finalizeRequest() {
387
  if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
388
  }
389
 
390
- // معالجة أخطاء الطلب
391
  function handleRequestError(error) {
392
  if (streamMsg) {
393
  streamMsg.querySelector('.loading')?.remove();
@@ -405,17 +424,19 @@ function handleRequestError(error) {
405
  alert(`Error: ${error.message || 'An error occurred during the request.'}`);
406
  isRequestActive = false;
407
  abortController = null;
 
 
 
408
  if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex';
409
  if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
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();
@@ -449,17 +470,14 @@ async function loadConversations() {
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();
@@ -478,19 +496,16 @@ async function loadConversation(conversationId) {
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) {
@@ -506,32 +521,9 @@ async function deleteConversation(conversationId) {
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;
@@ -541,7 +533,7 @@ async function createNewConversation() {
541
  method: 'POST',
542
  headers: {
543
  'Content-Type': 'application/json',
544
- 'Authorization': `Bearer ${checkAuth()}`
545
  },
546
  body: JSON.stringify({ title: 'New Conversation' })
547
  });
@@ -556,7 +548,7 @@ async function createNewConversation() {
556
  currentConversationId = data.conversation_id;
557
  currentConversationTitle = data.title;
558
  conversationHistory = [];
559
- sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
560
  if (uiElements.chatBox) uiElements.chatBox.innerHTML = '';
561
  if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
562
  history.pushState(null, '', `/chat/${currentConversationId}`);
@@ -573,24 +565,18 @@ async function createNewConversation() {
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;
@@ -601,7 +587,29 @@ async function updateConversationTitle(conversationId, newTitle) {
601
  }
602
  }
603
 
604
- // إعداد إيماءات اللمس
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  function setupTouchGestures() {
606
  if (!uiElements.sidebar) return;
607
  const hammer = new Hammer(uiElements.sidebar);
@@ -652,12 +660,9 @@ function setupTouchGestures() {
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;
@@ -665,7 +670,6 @@ async function submitMessage() {
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');
@@ -697,20 +701,21 @@ async function submitMessage() {
697
  system_prompt: isArabicText(message)
698
  ? 'أنت مساعد ذكي تقدم إجابات مفصلة ومنظمة باللغة العربية، مع ضمان الدقة والوضوح.'
699
  : 'You are an expert assistant providing detailed, comprehensive, and well-structured responses.',
700
- history: conversationHistory,
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
  }
709
 
710
  enterChatView();
711
  addMsg('user', message);
712
- conversationHistory.push({ role: 'user', content: message });
713
- sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
 
 
714
  streamMsg = addMsg('assistant', '');
715
  const loadingEl = document.createElement('span');
716
  loadingEl.className = 'loading';
@@ -769,13 +774,9 @@ async function submitMessage() {
769
  renderMarkdown(streamMsg);
770
  streamMsg.dataset.done = '1';
771
  }
772
- conversationHistory.push({ role: 'assistant', content: responseText });
773
- sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
774
- if (checkAuth() && currentConversationId) {
775
- await saveMessageToConversation(currentConversationId, 'assistant', responseText);
776
- }
777
- if (checkAuth()) {
778
- await loadConversations();
779
  }
780
  finalizeRequest();
781
  } catch (error) {
@@ -783,7 +784,7 @@ async function submitMessage() {
783
  }
784
  }
785
 
786
- // إيقاف البث
787
  function stopStream(forceCancel = false) {
788
  if (!isRequestActive && !isRecording) return;
789
  if (isRecording) stopVoiceRecording();
@@ -804,7 +805,7 @@ function stopStream(forceCancel = false) {
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 () => {
@@ -829,17 +830,17 @@ if (logoutBtn) {
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) {
@@ -891,42 +892,46 @@ if (uiElements.closeSettingsBtn) {
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,15 +947,15 @@ if (uiElements.historyToggle) {
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
  });
@@ -961,63 +966,49 @@ if (uiElements.fileInput) uiElements.fileInput.addEventListener('change', previe
961
  if (uiElements.audioInput) uiElements.audioInput.addEventListener('change', previewFile);
962
 
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;
974
- uiElements.sendBtn.addEventListener('mousedown', (e) => {
975
- if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
976
- pressTimer = setTimeout(() => {
977
- startVoiceRecording();
978
- }, 500);
979
- });
980
- uiElements.sendBtn.addEventListener('mouseup', () => {
981
- clearTimeout(pressTimer);
982
- if (isRecording) stopVoiceRecording();
983
- });
984
- uiElements.sendBtn.addEventListener('mouseleave', () => {
985
- clearTimeout(pressTimer);
986
- if (isRecording) stopVoiceRecording();
987
- });
988
-
989
- uiElements.sendBtn.addEventListener('touchstart', (e) => {
990
  e.preventDefault();
991
  if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
992
- pressTimer = setTimeout(() => {
993
- startVoiceRecording();
994
- }, 500);
995
- });
996
- uiElements.sendBtn.addEventListener('touchend', (e) => {
997
- e.preventDefault();
998
- clearTimeout(pressTimer);
999
- if (isRecording) stopVoiceRecording();
1000
- });
1001
- uiElements.sendBtn.addEventListener('touchcancel', (e) => {
1002
  e.preventDefault();
1003
  clearTimeout(pressTimer);
1004
  if (isRecording) stopVoiceRecording();
1005
- });
 
 
 
 
 
 
 
 
1006
  }
1007
 
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) {
@@ -1038,7 +1029,7 @@ if (uiElements.clearBtn) uiElements.clearBtn.addEventListener('click', clearAllM
1038
 
1039
  if (uiElements.conversationTitle) {
1040
  uiElements.conversationTitle.addEventListener('click', () => {
1041
- if (!checkAuth()) return alert('Please log in to edit the conversation title.');
1042
  const newTitle = prompt('Enter new conversation title:', currentConversationTitle || '');
1043
  if (newTitle && currentConversationId) {
1044
  updateConversationTitle(currentConversationId, newTitle);
@@ -1051,17 +1042,24 @@ if (uiElements.sidebarToggle) {
1051
  }
1052
 
1053
  if (uiElements.newConversationBtn) {
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');
 
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
  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
  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
  offset: 50,
58
  });
59
 
60
+ if (await checkAuth() && currentConversationId) {
 
61
  console.log('Loading conversation with ID:', currentConversationId);
62
  await loadConversation(currentConversationId);
63
+ } else if (!(await checkAuth()) && conversationHistory.length > 0) {
64
  console.log('Restoring conversation history from sessionStorage:', conversationHistory);
65
  enterChatView();
66
  conversationHistory.forEach(msg => {
 
81
  setupTouchGestures();
82
  });
83
 
84
+ // Check authentication token
85
+ async function checkAuth() {
86
  const token = localStorage.getItem('token');
87
+ if (!token) {
88
+ console.log('No auth token found');
89
+ return false;
90
+ }
91
+ try {
92
+ const response = await fetch('/api/verify-token', {
93
+ headers: { 'Authorization': `Bearer ${token}` }
94
+ });
95
+ if (response.ok) {
96
+ console.log('Auth token verified');
97
+ return true;
98
+ } else {
99
+ console.log('Token verification failed:', response.status);
100
+ localStorage.removeItem('token');
101
+ return false;
102
+ }
103
+ } catch (error) {
104
+ console.error('Error verifying token:', error);
105
+ localStorage.removeItem('token');
106
+ return false;
107
+ }
108
  }
109
 
110
+ // Handle session for non-logged-in users
111
  async function handleSession() {
112
  const sessionId = sessionStorage.getItem('session_id');
113
  if (!sessionId) {
 
120
  return sessionId;
121
  }
122
 
123
+ // Update send button state
124
  function updateSendButtonState() {
125
  if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) {
126
  const hasInput = uiElements.input.value.trim() !== '' ||
127
  uiElements.fileInput.files.length > 0 ||
128
  uiElements.audioInput.files.length > 0;
129
  uiElements.sendBtn.disabled = !hasInput || isRequestActive || isRecording;
130
+ console.log('Send button state:', { hasInput, isRequestActive, isRecording, disabled: uiElements.sendBtn.disabled });
131
  }
132
  }
133
 
134
+ // Render markdown content with RTL support
135
  function renderMarkdown(el) {
136
  const raw = el.dataset.text || '';
137
  const isArabic = isArabicText(raw);
 
162
  }
163
  }
164
 
165
+ // Toggle chat view
166
  function enterChatView() {
167
  if (uiElements.chatHeader) {
168
  uiElements.chatHeader.classList.remove('hidden');
 
175
  if (uiElements.initialContent) uiElements.initialContent.classList.add('hidden');
176
  }
177
 
178
+ // Toggle home view
179
  function leaveChatView() {
180
  if (uiElements.chatHeader) {
181
  uiElements.chatHeader.classList.add('hidden');
 
185
  if (uiElements.initialContent) uiElements.initialContent.classList.remove('hidden');
186
  }
187
 
188
+ // Add chat bubble
189
  function addMsg(who, text) {
190
  const div = document.createElement('div');
191
  div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`;
 
201
  return div;
202
  }
203
 
204
+ // Clear all messages
205
  function clearAllMessages() {
206
  stopStream(true);
207
  conversationHistory = [];
208
+ sessionStorage.removeItem('conversationHistory');
209
  currentAssistantText = '';
210
  if (streamMsg) {
211
  streamMsg.querySelector('.loading')?.remove();
 
226
  autoResizeTextarea();
227
  }
228
 
229
+ // File preview
230
  function previewFile() {
231
  if (uiElements.fileInput?.files.length > 0) {
232
  const file = uiElements.fileInput.files[0];
 
260
  }
261
  }
262
 
263
+ // Voice recording
264
  function startVoiceRecording() {
265
  if (isRequestActive || isRecording) {
266
  console.log('Voice recording blocked: Request active or already recording');
 
301
  }
302
  }
303
 
304
+ // Send audio message
305
  async function submitAudioMessage(formData) {
306
  enterChatView();
307
  addMsg('user', 'Voice message');
308
+ if (!(await checkAuth())) {
309
+ conversationHistory.push({ role: 'user', content: 'Voice message' });
310
+ sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
311
+ }
312
  streamMsg = addMsg('assistant', '');
313
  const loadingEl = document.createElement('span');
314
  loadingEl.className = 'loading';
 
331
  renderMarkdown(streamMsg);
332
  streamMsg.dataset.done = '1';
333
  }
334
+ if (!(await checkAuth())) {
335
+ conversationHistory.push({ role: 'assistant', content: transcription });
336
+ sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
 
337
  }
338
  if (checkAuth() && data.conversation_id) {
339
  currentConversationId = data.conversation_id;
 
348
  }
349
  }
350
 
351
+ // Helper to send API requests
352
  async function sendRequest(endpoint, body, headers = {}) {
353
+ const token = localStorage.getItem('token');
354
  if (token) headers['Authorization'] = `Bearer ${token}`;
355
  headers['X-Session-ID'] = await handleSession();
356
  console.log('Sending request to:', endpoint, 'with headers:', headers);
 
386
  }
387
  }
388
 
389
+ // Helper to update UI during request
390
  function updateUIForRequest() {
391
  if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'inline-flex';
392
  if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'none';
 
397
  autoResizeTextarea();
398
  }
399
 
400
+ // Helper to finalize request
401
  function finalizeRequest() {
402
  streamMsg = null;
403
  isRequestActive = false;
 
406
  if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
407
  }
408
 
409
+ // Helper to handle request errors
410
  function handleRequestError(error) {
411
  if (streamMsg) {
412
  streamMsg.querySelector('.loading')?.remove();
 
424
  alert(`Error: ${error.message || 'An error occurred during the request.'}`);
425
  isRequestActive = false;
426
  abortController = null;
427
+ if (!(checkAuth())) {
428
+ sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
429
+ }
430
  if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex';
431
  if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
 
432
  }
433
 
434
+ // Load conversations for sidebar
435
  async function loadConversations() {
436
+ if (!(await checkAuth())) return;
437
  try {
438
  const response = await fetch('/api/conversations', {
439
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
440
  });
441
  if (!response.ok) throw new Error('Failed to load conversations');
442
  const conversations = await response.json();
 
470
  }
471
  }
472
 
473
+ // Load conversation from API
474
  async function loadConversation(conversationId) {
475
  try {
476
  const response = await fetch(`/api/conversations/${conversationId}`, {
477
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
478
  });
479
  if (!response.ok) {
480
+ if (response.status === 401) window.location.href = '/login';
 
 
 
481
  throw new Error('Failed to load conversation');
482
  }
483
  const data = await response.json();
 
496
  }
497
  }
498
 
499
+ // Delete conversation
500
  async function deleteConversation(conversationId) {
501
  if (!confirm('Are you sure you want to delete this conversation?')) return;
502
  try {
503
  const response = await fetch(`/api/conversations/${conversationId}`, {
504
  method: 'DELETE',
505
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
506
  });
507
  if (!response.ok) {
508
+ if (response.status === 401) window.location.href = '/login';
 
 
 
509
  throw new Error('Failed to delete conversation');
510
  }
511
  if (conversationId === currentConversationId) {
 
521
  }
522
  }
523
 
524
+ // Create new conversation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
  async function createNewConversation() {
526
+ if (!(await checkAuth())) {
527
  alert('Please log in to create a new conversation.');
528
  window.location.href = '/login';
529
  return;
 
533
  method: 'POST',
534
  headers: {
535
  'Content-Type': 'application/json',
536
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
537
  },
538
  body: JSON.stringify({ title: 'New Conversation' })
539
  });
 
548
  currentConversationId = data.conversation_id;
549
  currentConversationTitle = data.title;
550
  conversationHistory = [];
551
+ sessionStorage.removeItem('conversationHistory');
552
  if (uiElements.chatBox) uiElements.chatBox.innerHTML = '';
553
  if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
554
  history.pushState(null, '', `/chat/${currentConversationId}`);
 
565
  });
566
  }
567
 
568
+ // Update conversation title
569
  async function updateConversationTitle(conversationId, newTitle) {
570
  try {
571
  const response = await fetch(`/api/conversations/${conversationId}/title`, {
572
  method: 'PUT',
573
  headers: {
574
  'Content-Type': 'application/json',
575
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
576
  },
577
  body: JSON.stringify({ title: newTitle })
578
  });
579
+ if (!response.ok) throw new Error('Failed to update title');
 
 
 
 
 
 
580
  const data = await response.json();
581
  currentConversationTitle = data.title;
582
  if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
 
587
  }
588
  }
589
 
590
+ // Toggle sidebar
591
+ function toggleSidebar(show) {
592
+ if (uiElements.sidebar) {
593
+ if (window.innerWidth >= 768) {
594
+ isSidebarOpen = true;
595
+ uiElements.sidebar.style.transform = 'translateX(0)';
596
+ if (uiElements.swipeHint) uiElements.swipeHint.style.display = 'none';
597
+ } else {
598
+ isSidebarOpen = show !== undefined ? show : !isSidebarOpen;
599
+ uiElements.sidebar.style.transform = isSidebarOpen ? 'translateX(0)' : 'translateX(-100%)';
600
+ if (uiElements.swipeHint && !isSidebarOpen) {
601
+ uiElements.swipeHint.style.display = 'block';
602
+ setTimeout(() => {
603
+ uiElements.swipeHint.style.display = 'none';
604
+ }, 3000);
605
+ } else if (uiElements.swipeHint) {
606
+ uiElements.swipeHint.style.display = 'none';
607
+ }
608
+ }
609
+ }
610
+ }
611
+
612
+ // Setup touch gestures with Hammer.js
613
  function setupTouchGestures() {
614
  if (!uiElements.sidebar) return;
615
  const hammer = new Hammer(uiElements.sidebar);
 
660
  });
661
  }
662
 
663
+ // Send user message
664
  async function submitMessage() {
665
+ if (isRequestActive || isRecording) return;
 
 
 
666
  let message = uiElements.input?.value.trim() || '';
667
  let payload = null;
668
  let formData = null;
 
670
  let headers = {};
671
  let inputType = 'text';
672
  let outputFormat = 'text';
 
673
 
674
  if (!message && !uiElements.fileInput?.files.length && !uiElements.audioInput?.files.length) {
675
  console.log('No message, file, or audio to send');
 
701
  system_prompt: isArabicText(message)
702
  ? 'أنت مساعد ذكي تقدم إجابات مفصلة ومنظمة باللغة العربية، مع ضمان الدقة والوضوح.'
703
  : 'You are an expert assistant providing detailed, comprehensive, and well-structured responses.',
704
+ history: (await checkAuth()) ? [] : conversationHistory,
705
  temperature: 0.7,
706
  max_new_tokens: 128000,
707
  enable_browsing: true,
708
+ output_format: 'text'
 
709
  };
710
  headers['Content-Type'] = 'application/json';
711
  }
712
 
713
  enterChatView();
714
  addMsg('user', message);
715
+ if (!(await checkAuth())) {
716
+ conversationHistory.push({ role: 'user', content: message });
717
+ sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
718
+ }
719
  streamMsg = addMsg('assistant', '');
720
  const loadingEl = document.createElement('span');
721
  loadingEl.className = 'loading';
 
774
  renderMarkdown(streamMsg);
775
  streamMsg.dataset.done = '1';
776
  }
777
+ if (!(await checkAuth())) {
778
+ conversationHistory.push({ role: 'assistant', content: responseText });
779
+ sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
 
 
 
 
780
  }
781
  finalizeRequest();
782
  } catch (error) {
 
784
  }
785
  }
786
 
787
+ // Stop streaming
788
  function stopStream(forceCancel = false) {
789
  if (!isRequestActive && !isRecording) return;
790
  if (isRecording) stopVoiceRecording();
 
805
  if (uiElements.stopBtn) uiElements.stopBtn.style.pointerEvents = 'auto';
806
  }
807
 
808
+ // Logout handler
809
  const logoutBtn = document.querySelector('#logoutBtn');
810
  if (logoutBtn) {
811
  logoutBtn.addEventListener('click', async () => {
 
830
  });
831
  }
832
 
833
+ // Settings Modal
834
  if (uiElements.settingsBtn) {
835
  uiElements.settingsBtn.addEventListener('click', async () => {
836
+ if (!(await checkAuth())) {
837
  alert('Please log in to access settings.');
838
  window.location.href = '/login';
839
  return;
840
  }
841
  try {
842
  const response = await fetch('/api/settings', {
843
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
844
  });
845
  if (!response.ok) {
846
  if (response.status === 401) {
 
892
  }
893
 
894
  if (uiElements.settingsForm) {
895
+ uiElements.settingsForm.addEventListener('submit', (e) => {
896
  e.preventDefault();
897
+ if (!(checkAuth())) {
898
  alert('Please log in to save settings.');
899
  window.location.href = '/login';
900
  return;
901
  }
902
  const formData = new FormData(uiElements.settingsForm);
903
  const data = Object.fromEntries(formData);
904
+ fetch('/users/me', {
905
+ method: 'PUT',
906
+ headers: {
907
+ 'Content-Type': 'application/json',
908
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
909
+ },
910
+ body: JSON.stringify(data)
911
+ })
912
+ .then(res => {
913
+ if (!res.ok) {
914
+ if (res.status === 401) {
915
+ localStorage.removeItem('token');
916
+ window.location.href = '/login';
917
+ }
918
+ throw new Error('Failed to update settings');
919
  }
920
+ return res.json();
921
+ })
922
+ .then(() => {
923
+ alert('Settings updated successfully!');
924
+ uiElements.settingsModal.classList.add('hidden');
925
+ toggleSidebar(false);
926
+ })
927
+ .catch(err => {
928
+ console.error('Error updating settings:', err);
929
+ alert('Error updating settings: ' + err.message);
930
+ });
931
  });
932
  }
933
 
934
+ // History Toggle
935
  if (uiElements.historyToggle) {
936
  uiElements.historyToggle.addEventListener('click', () => {
937
  if (uiElements.conversationList) {
 
947
  });
948
  }
949
 
950
+ // Event listeners
951
  uiElements.promptItems.forEach(p => {
952
  p.addEventListener('click', e => {
953
  e.preventDefault();
954
  if (uiElements.input) {
955
  uiElements.input.value = p.dataset.prompt;
956
  autoResizeTextarea();
 
957
  }
958
+ if (uiElements.sendBtn) uiElements.sendBtn.disabled = false;
959
  submitMessage();
960
  });
961
  });
 
966
  if (uiElements.audioInput) uiElements.audioInput.addEventListener('change', previewFile);
967
 
968
  if (uiElements.sendBtn) {
 
 
 
 
 
 
 
 
 
969
  let pressTimer;
970
+ const handlePressStart = (e) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
971
  e.preventDefault();
972
  if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
973
+ if (uiElements.input.value.trim() || uiElements.fileInput.files.length > 0 || uiElements.audioInput.files.length > 0) {
974
+ submitMessage();
975
+ } else {
976
+ pressTimer = setTimeout(() => startVoiceRecording(), 500);
977
+ }
978
+ };
979
+
980
+ const handlePressEnd = (e) => {
 
 
981
  e.preventDefault();
982
  clearTimeout(pressTimer);
983
  if (isRecording) stopVoiceRecording();
984
+ };
985
+
986
+ uiElements.sendBtn.replaceWith(uiElements.sendBtn.cloneNode(true));
987
+ uiElements.sendBtn = document.getElementById('sendBtn');
988
+
989
+ uiElements.sendBtn.addEventListener('click', handlePressStart);
990
+ uiElements.sendBtn.addEventListener('touchstart', handlePressStart);
991
+ uiElements.sendBtn.addEventListener('touchend', handlePressEnd);
992
+ uiElements.sendBtn.addEventListener('touchcancel', handlePressEnd);
993
  }
994
 
995
  if (uiElements.form) {
996
  uiElements.form.addEventListener('submit', (e) => {
997
  e.preventDefault();
998
+ if (!isRecording && uiElements.input.value.trim()) {
999
  submitMessage();
1000
  }
1001
  });
1002
  }
1003
 
1004
  if (uiElements.input) {
1005
+ let debounceTimer;
1006
  uiElements.input.addEventListener('input', () => {
1007
+ clearTimeout(debounceTimer);
1008
+ debounceTimer = setTimeout(() => {
1009
+ autoResizeTextarea();
1010
+ updateSendButtonState();
1011
+ }, 100);
1012
  });
1013
  uiElements.input.addEventListener('keydown', (e) => {
1014
  if (e.key === 'Enter' && !e.shiftKey) {
 
1029
 
1030
  if (uiElements.conversationTitle) {
1031
  uiElements.conversationTitle.addEventListener('click', () => {
1032
+ if (!(checkAuth())) return alert('Please log in to edit the conversation title.');
1033
  const newTitle = prompt('Enter new conversation title:', currentConversationTitle || '');
1034
  if (newTitle && currentConversationId) {
1035
  updateConversationTitle(currentConversationId, newTitle);
 
1042
  }
1043
 
1044
  if (uiElements.newConversationBtn) {
1045
+ uiElements.newConversationBtn.addEventListener('click', async () => {
1046
+ if (!(await checkAuth())) {
1047
+ alert('Please log in to create a new conversation.');
1048
+ window.location.href = '/login';
1049
+ return;
1050
+ }
1051
+ await createNewConversation();
1052
+ });
1053
  }
1054
 
1055
+ // Debug localStorage
1056
  const originalRemoveItem = localStorage.removeItem;
1057
  localStorage.removeItem = function (key) {
1058
  console.log('Removing from localStorage:', key);
1059
  originalRemoveItem.apply(this, arguments);
1060
  };
1061
 
1062
+ // Offline mode detection
1063
  window.addEventListener('offline', () => {
1064
  if (uiElements.messageLimitWarning) {
1065
  uiElements.messageLimitWarning.classList.remove('hidden');