Mark-Lasfar commited on
Commit
596833b
·
1 Parent(s): aec7386
Files changed (3) hide show
  1. static/css/sidebar.css +10 -14
  2. static/js/chat.js +74 -275
  3. templates/chat.html +37 -8
static/css/sidebar.css CHANGED
@@ -1,11 +1,8 @@
 
1
  #sidebar {
2
  background: linear-gradient(to bottom, rgba(31, 41, 55, 0.9), rgba(20, 78, 85, 0.9));
3
- backdrop-filter: blur(5px); /* قللنا الـ blur من 10px إلى 5px */
4
- -webkit-backdrop-filter: blur(5px);
5
- box-shadow: 2px 0 5px rgba(0, 0, 0, 0.2); /* قللنا الـ shadow */
6
- transition: transform 0.2s ease-in-out; /* قللنا المدة من 0.3s إلى 0.2s */
7
- will-change: transform; /* تحسين الأداء */
8
- touch-action: pan-y; /* منع الـ scroll أثناء السحب */
9
  }
10
 
11
  #sidebar nav ul li a {
@@ -13,12 +10,11 @@
13
  align-items: center;
14
  padding: 0.5rem 1rem;
15
  border-radius: 0.375rem;
16
- transition: background-color 0.15s ease, transform 0.15s ease; /* تسريع الـ transition */
17
  }
18
 
19
  #sidebar nav ul li a:hover {
20
  background-color: rgba(55, 65, 81, 0.8);
21
- transform: translateX(2px); /* قللنا التحريك من 4px إلى 2px */
22
  }
23
 
24
  #conversationList li {
@@ -27,12 +23,11 @@
27
  justify-content: space-between;
28
  padding: 0.5rem 1rem;
29
  border-radius: 0.375rem;
30
- transition: background-color 0.15s ease, transform 0.15s ease;
31
  }
32
 
33
  #conversationList li:hover {
34
  background-color: rgba(55, 65, 81, 0.8);
35
- transform: translateX(2px);
36
  }
37
 
38
  #conversationList li span.truncate {
@@ -44,7 +39,7 @@
44
 
45
  .delete-conversation-btn {
46
  opacity: 0;
47
- transition: opacity 0.15s ease;
48
  }
49
 
50
  #conversationList li:hover .delete-conversation-btn {
@@ -57,14 +52,15 @@
57
 
58
  @keyframes pulse {
59
  0% { transform: scale(1); opacity: 0.7; }
60
- 50% { transform: scale(1.1); opacity: 1; } /* قللنا الـ scale من 1.2 إلى 1.1 */
61
  100% { transform: scale(1); opacity: 0.7; }
62
  }
63
 
 
64
  @media (max-width: 768px) {
65
  #sidebar {
66
- width: 75%; /* قللنا العرض من 80% إلى 75% */
67
- max-width: 260px; /* قللنا العرض الأقصى */
68
  }
69
  .md\:ml-64 {
70
  margin-left: 0 !important;
 
1
+ /* Sidebar styles */
2
  #sidebar {
3
  background: linear-gradient(to bottom, rgba(31, 41, 55, 0.9), rgba(20, 78, 85, 0.9));
4
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.3);
5
+ transition: transform 0.2s ease-in-out; /* Shortened transition for faster response */
 
 
 
 
6
  }
7
 
8
  #sidebar nav ul li a {
 
10
  align-items: center;
11
  padding: 0.5rem 1rem;
12
  border-radius: 0.375rem;
13
+ transition: background-color 0.2s ease; /* Removed transform for performance */
14
  }
15
 
16
  #sidebar nav ul li a:hover {
17
  background-color: rgba(55, 65, 81, 0.8);
 
18
  }
19
 
20
  #conversationList li {
 
23
  justify-content: space-between;
24
  padding: 0.5rem 1rem;
25
  border-radius: 0.375rem;
26
+ transition: background-color 0.2s ease; /* Removed transform for performance */
27
  }
28
 
29
  #conversationList li:hover {
30
  background-color: rgba(55, 65, 81, 0.8);
 
31
  }
32
 
33
  #conversationList li span.truncate {
 
39
 
40
  .delete-conversation-btn {
41
  opacity: 0;
42
+ transition: opacity 0.2s ease;
43
  }
44
 
45
  #conversationList li:hover .delete-conversation-btn {
 
52
 
53
  @keyframes pulse {
54
  0% { transform: scale(1); opacity: 0.7; }
55
+ 50% { transform: scale(1.2); opacity: 1; }
56
  100% { transform: scale(1); opacity: 0.7; }
57
  }
58
 
59
+ /* Responsive adjustments */
60
  @media (max-width: 768px) {
61
  #sidebar {
62
+ width: 80%;
63
+ max-width: 280px;
64
  }
65
  .md\:ml-64 {
66
  margin-left: 0 !important;
static/js/chat.js CHANGED
@@ -50,16 +50,6 @@ let isSidebarOpen = window.innerWidth >= 768;
50
 
51
  // Initialize AOS and load initial conversation
52
  document.addEventListener('DOMContentLoaded', async () => {
53
-
54
- if (!uiElements.sendBtn || !uiElements.input) {
55
- console.error('Critical UI elements missing:', {
56
- sendBtn: !!uiElements.sendBtn,
57
- input: !!uiElements.input
58
- });
59
- alert('Error: Required UI elements are missing. Please reload the page.');
60
- return;
61
- }
62
-
63
  AOS.init({
64
  duration: 800,
65
  easing: 'ease-out-cubic',
@@ -68,17 +58,12 @@ document.addEventListener('DOMContentLoaded', async () => {
68
  });
69
 
70
  if (currentConversationId && checkAuth()) {
71
- console.log('Loading conversation with ID:', currentConversationId);
72
  await loadConversation(currentConversationId);
73
  } else if (conversationHistory.length > 0) {
74
- console.log('Restoring conversation history from sessionStorage:', conversationHistory);
75
  enterChatView();
76
  conversationHistory.forEach(msg => {
77
- console.log('Adding message from history:', msg);
78
  addMsg(msg.role, msg.content);
79
  });
80
- } else {
81
- console.log('No conversation history or ID, starting fresh');
82
  }
83
 
84
  autoResizeTextarea();
@@ -89,17 +74,28 @@ document.addEventListener('DOMContentLoaded', async () => {
89
  }, 3000);
90
  }
91
  setupTouchGestures();
 
92
  });
93
 
94
- // Check authentication token
95
  function checkAuth() {
96
  const token = localStorage.getItem('token');
97
- console.log('Checking auth token:', { token: token ? 'Found' : 'Not found', value: token });
98
- if (!token) {
99
- console.warn('No auth token found, redirecting to login');
100
- window.location.href = '/login';
 
 
 
 
 
 
 
 
 
 
101
  }
102
- return token;
103
  }
104
 
105
  // Handle session for non-logged-in users
@@ -108,33 +104,18 @@ async function handleSession() {
108
  if (!sessionId) {
109
  const newSessionId = crypto.randomUUID();
110
  sessionStorage.setItem('session_id', newSessionId);
111
- console.log('New session_id created:', newSessionId);
112
  return newSessionId;
113
  }
114
- console.log('Existing session_id:', sessionId);
115
  return sessionId;
116
  }
117
 
118
- // Update send button state
119
  function updateSendButtonState() {
120
  if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) {
121
- const hasInput = uiElements.input.value.trim() !== '' ||
122
- uiElements.fileInput.files.length > 0 ||
123
- uiElements.audioInput.files.length > 0;
124
  uiElements.sendBtn.disabled = !hasInput;
125
- console.log('Send button state:', {
126
- input: uiElements.input.value.trim(),
127
- files: uiElements.fileInput.files.length,
128
- audio: uiElements.audioInput.files.length,
129
- disabled: uiElements.sendBtn.disabled
130
- });
131
- } else {
132
- console.error('UI elements missing:', {
133
- sendBtn: !!uiElements.sendBtn,
134
- input: !!uiElements.input,
135
- fileInput: !!uiElements.fileInput,
136
- audioInput: !!uiElements.audioInput
137
- });
138
  }
139
  }
140
 
@@ -161,7 +142,6 @@ function renderMarkdown(el) {
161
  });
162
  wrapper.querySelectorAll('hr').forEach(h => h.classList.add('styled-hr'));
163
  Prism.highlightAllUnder(wrapper);
164
- // Smooth scroll to bottom
165
  if (uiElements.chatBox) {
166
  uiElements.chatBox.scrollTo({
167
  top: uiElements.chatBox.scrollHeight,
@@ -198,13 +178,10 @@ function addMsg(who, text) {
198
  const div = document.createElement('div');
199
  div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`;
200
  div.dataset.text = text;
201
- console.log('Adding message:', { who, text });
202
  renderMarkdown(div);
203
  if (uiElements.chatBox) {
204
  uiElements.chatBox.appendChild(div);
205
  uiElements.chatBox.classList.remove('hidden');
206
- } else {
207
- console.error('chatBox is null');
208
  }
209
  return div;
210
  }
@@ -270,24 +247,17 @@ function previewFile() {
270
 
271
  // Voice recording
272
  function startVoiceRecording() {
273
- if (isRequestActive || isRecording) {
274
- console.log('Voice recording blocked: Request active or already recording');
275
- return;
276
- }
277
- console.log('Starting voice recording...');
278
  isRecording = true;
279
  if (uiElements.sendBtn) uiElements.sendBtn.classList.add('recording');
280
  navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
281
  mediaRecorder = new MediaRecorder(stream);
282
  audioChunks = [];
283
  mediaRecorder.start();
284
- console.log('MediaRecorder started');
285
  mediaRecorder.addEventListener('dataavailable', event => {
286
  audioChunks.push(event.data);
287
- console.log('Audio chunk received:', event.data);
288
  });
289
  }).catch(err => {
290
- console.error('Error accessing microphone:', err);
291
  alert('Failed to access microphone. Please check permissions.');
292
  isRecording = false;
293
  if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
@@ -300,7 +270,6 @@ function stopVoiceRecording() {
300
  if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
301
  isRecording = false;
302
  mediaRecorder.addEventListener('stop', async () => {
303
- console.log('Stopping voice recording, sending audio...');
304
  const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
305
  const formData = new FormData();
306
  formData.append('file', audioBlob, 'voice-message.webm');
@@ -357,10 +326,9 @@ async function submitAudioMessage(formData) {
357
 
358
  // Helper to send API requests
359
  async function sendRequest(endpoint, body, headers = {}) {
360
- const token = checkAuth();
361
  if (token) headers['Authorization'] = `Bearer ${token}`;
362
  headers['X-Session-ID'] = await handleSession();
363
- console.log('Sending request to:', endpoint, 'with headers:', headers);
364
  try {
365
  const response = await fetch(endpoint, {
366
  method: 'POST',
@@ -385,7 +353,6 @@ async function sendRequest(endpoint, body, headers = {}) {
385
  }
386
  return response;
387
  } catch (error) {
388
- console.error('Send request error:', error);
389
  if (error.name === 'AbortError') {
390
  throw new Error('Request was aborted');
391
  }
@@ -427,7 +394,6 @@ function handleRequestError(error) {
427
  streamMsg.dataset.done = '1';
428
  streamMsg = null;
429
  }
430
- console.error('Request error:', error);
431
  alert(`Error: ${error.message || 'An error occurred during the request.'}`);
432
  isRequestActive = false;
433
  abortController = null;
@@ -441,7 +407,7 @@ async function loadConversations() {
441
  if (!checkAuth()) return;
442
  try {
443
  const response = await fetch('/api/conversations', {
444
- headers: { 'Authorization': `Bearer ${checkAuth()}` }
445
  });
446
  if (!response.ok) throw new Error('Failed to load conversations');
447
  const conversations = await response.json();
@@ -470,7 +436,6 @@ async function loadConversations() {
470
  });
471
  }
472
  } catch (error) {
473
- console.error('Error loading conversations:', error);
474
  alert('Failed to load conversations. Please try again.');
475
  }
476
  }
@@ -479,7 +444,7 @@ async function loadConversations() {
479
  async function loadConversation(conversationId) {
480
  try {
481
  const response = await fetch(`/api/conversations/${conversationId}`, {
482
- headers: { 'Authorization': `Bearer ${checkAuth()}` }
483
  });
484
  if (!response.ok) {
485
  if (response.status === 401) window.location.href = '/login';
@@ -496,7 +461,6 @@ async function loadConversation(conversationId) {
496
  history.pushState(null, '', `/chat/${currentConversationId}`);
497
  toggleSidebar(false);
498
  } catch (error) {
499
- console.error('Error loading conversation:', error);
500
  alert('Failed to load conversation. Please try again or log in.');
501
  }
502
  }
@@ -507,7 +471,7 @@ async function deleteConversation(conversationId) {
507
  try {
508
  const response = await fetch(`/api/conversations/${conversationId}`, {
509
  method: 'DELETE',
510
- headers: { 'Authorization': `Bearer ${checkAuth()}` }
511
  });
512
  if (!response.ok) {
513
  if (response.status === 401) window.location.href = '/login';
@@ -521,7 +485,6 @@ async function deleteConversation(conversationId) {
521
  }
522
  await loadConversations();
523
  } catch (error) {
524
- console.error('Error deleting conversation:', error);
525
  alert('Failed to delete conversation. Please try again.');
526
  }
527
  }
@@ -533,7 +496,7 @@ async function saveMessageToConversation(conversationId, role, content) {
533
  method: 'POST',
534
  headers: {
535
  'Content-Type': 'application/json',
536
- 'Authorization': `Bearer ${checkAuth()}`
537
  },
538
  body: JSON.stringify({ role, content })
539
  });
@@ -543,10 +506,9 @@ async function saveMessageToConversation(conversationId, role, content) {
543
  }
544
  }
545
 
546
- // Create new conversation
547
  async function createNewConversation() {
548
  if (!checkAuth()) {
549
- alert('Please log in to create a new conversation.');
550
  window.location.href = '/login';
551
  return;
552
  }
@@ -555,7 +517,7 @@ async function createNewConversation() {
555
  method: 'POST',
556
  headers: {
557
  'Content-Type': 'application/json',
558
- 'Authorization': `Bearer ${checkAuth()}`
559
  },
560
  body: JSON.stringify({ title: 'New Conversation' })
561
  });
@@ -578,7 +540,6 @@ async function createNewConversation() {
578
  await loadConversations();
579
  toggleSidebar(false);
580
  } catch (error) {
581
- console.error('Error creating conversation:', error);
582
  alert('Failed to create new conversation. Please try again.');
583
  }
584
  if (uiElements.chatBox) uiElements.chatBox.scrollTo({
@@ -594,7 +555,7 @@ async function updateConversationTitle(conversationId, newTitle) {
594
  method: 'PUT',
595
  headers: {
596
  'Content-Type': 'application/json',
597
- 'Authorization': `Bearer ${checkAuth()}`
598
  },
599
  body: JSON.stringify({ title: newTitle })
600
  });
@@ -604,88 +565,55 @@ async function updateConversationTitle(conversationId, newTitle) {
604
  if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
605
  await loadConversations();
606
  } catch (error) {
607
- console.error('Error updating title:', error);
608
  alert('Failed to update conversation title.');
609
  }
610
  }
611
 
612
- // Toggle sidebar
613
  function toggleSidebar(show) {
614
  if (uiElements.sidebar) {
615
- if (window.innerWidth >= 768) {
616
- isSidebarOpen = true;
617
- uiElements.sidebar.style.transform = 'translateX(0)';
618
- if (uiElements.swipeHint) uiElements.swipeHint.style.display = 'none';
619
- } else {
620
- isSidebarOpen = show !== undefined ? show : !isSidebarOpen;
621
- uiElements.sidebar.style.transform = isSidebarOpen ? 'translateX(0)' : 'translateX(-100%)';
622
- if (uiElements.swipeHint && !isSidebarOpen) {
623
- uiElements.swipeHint.style.display = 'block';
624
- setTimeout(() => {
625
- uiElements.swipeHint.style.display = 'none';
626
- }, 3000);
627
- } else if (uiElements.swipeHint) {
628
  uiElements.swipeHint.style.display = 'none';
629
- }
 
 
630
  }
631
  }
632
  }
633
 
634
- // Setup touch gestures with Hammer.js
635
  function setupTouchGestures() {
636
- if (!uiElements.sidebar) {
637
- console.error('Sidebar element not found');
638
- return;
639
- }
640
- const hammer = new Hammer(uiElements.sidebar, {
641
- touchAction: 'pan-y', // منع الـ scroll أثناء السحب
642
- threshold: 5, // تقليل الحساسية
643
- velocity: 0.3 // تسريع الاستجابة
644
- });
645
  const mainContent = document.querySelector('.flex-1');
646
- const hammerMain = new Hammer(mainContent, {
647
- touchAction: 'pan-y',
648
- threshold: 5,
649
- velocity: 0.3
650
- });
651
 
652
- hammer.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL });
653
  hammer.on('pan', e => {
654
  if (!isSidebarOpen) return;
655
  let translateX = Math.max(-uiElements.sidebar.offsetWidth, Math.min(0, e.deltaX));
656
  uiElements.sidebar.style.transform = `translateX(${translateX}px)`;
657
- uiElements.sidebar.style.transition = 'none';
658
  });
659
  hammer.on('panend', e => {
660
- uiElements.sidebar.style.transition = 'transform 0.2s ease-in-out';
661
- if (e.deltaX < -30) { // قللنا الـ threshold من -50 إلى -30
662
  toggleSidebar(false);
663
  } else {
664
  toggleSidebar(true);
665
  }
666
  });
667
 
668
- hammerMain.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL });
669
- hammerMain.on('panstart', e => {
670
- if (isSidebarOpen) return;
671
- if (e.center.x < 50 || e.center.x > window.innerWidth - 50) {
672
- uiElements.sidebar.style.transition = 'none';
673
- }
674
- });
675
  hammerMain.on('pan', e => {
676
  if (isSidebarOpen) return;
677
- if (e.center.x < 50 || e.center.x > window.innerWidth - 50) {
678
- let translateX = e.center.x < 50
679
- ? Math.min(uiElements.sidebar.offsetWidth, Math.max(0, e.deltaX))
680
- : Math.max(-uiElements.sidebar.offsetWidth, Math.min(0, e.deltaX));
681
- uiElements.sidebar.style.transform = `translateX(${translateX - uiElements.sidebar.offsetWidth}px)`;
682
- }
683
  });
684
  hammerMain.on('panend', e => {
685
- uiElements.sidebar.style.transition = 'transform 0.2s ease-in-out';
686
- if (e.center.x < 50 && e.deltaX > 30) {
687
- toggleSidebar(true);
688
- } else if (e.center.x > window.innerWidth - 50 && e.deltaX < -30) {
689
  toggleSidebar(true);
690
  } else {
691
  toggleSidebar(false);
@@ -693,32 +621,21 @@ function setupTouchGestures() {
693
  });
694
  }
695
 
696
- // Send user message
697
  async function submitMessage() {
698
- console.log('submitMessage called', { isRequestActive, isRecording, input: uiElements.input?.value.trim() });
699
- if (isRequestActive || isRecording) {
700
- console.warn('Submit blocked: Request active or recording');
701
- return;
702
- }
703
  let message = uiElements.input?.value.trim() || '';
704
- if (!message && !uiElements.fileInput?.files.length && !uiElements.audioInput?.files.length) {
705
- console.log('No valid input to send');
706
- return;
707
- }
708
-
709
  let payload = null;
710
  let formData = null;
711
  let endpoint = '/api/chat';
712
  let headers = {};
713
- let inputType = 'text';
714
- let outputFormat = 'text';
715
- let title = null;
716
 
717
  if (uiElements.fileInput?.files.length > 0) {
718
  const file = uiElements.fileInput.files[0];
719
  if (file.type.startsWith('image/')) {
720
  endpoint = '/api/image-analysis';
721
- inputType = 'image';
722
  message = 'Analyze this image';
723
  formData = new FormData();
724
  formData.append('file', file);
@@ -728,7 +645,6 @@ async function submitMessage() {
728
  const file = uiElements.audioInput.files[0];
729
  if (file.type.startsWith('audio/')) {
730
  endpoint = '/api/audio-transcription';
731
- inputType = 'audio';
732
  message = 'Transcribe this audio';
733
  formData = new FormData();
734
  formData.append('file', file);
@@ -743,8 +659,7 @@ async function submitMessage() {
743
  temperature: 0.7,
744
  max_new_tokens: 128000,
745
  enable_browsing: true,
746
- output_format: 'text',
747
- title: title
748
  };
749
  headers['Content-Type'] = 'application/json';
750
  }
@@ -763,7 +678,6 @@ async function submitMessage() {
763
  abortController = new AbortController();
764
 
765
  try {
766
- console.log('Sending request to:', endpoint);
767
  const response = await sendRequest(endpoint, payload ? JSON.stringify(payload) : formData, headers);
768
  let responseText = '';
769
  if (endpoint === '/api/audio-transcription') {
@@ -790,10 +704,7 @@ async function submitMessage() {
790
  let buffer = '';
791
  while (true) {
792
  const { done, value } = await reader.read();
793
- if (done) {
794
- if (!buffer.trim()) throw new Error('Empty response from server');
795
- break;
796
- }
797
  buffer += decoder.decode(value, { stream: true });
798
  if (streamMsg) {
799
  streamMsg.dataset.text = buffer;
@@ -850,7 +761,6 @@ function stopStream(forceCancel = false) {
850
  const logoutBtn = document.querySelector('#logoutBtn');
851
  if (logoutBtn) {
852
  logoutBtn.addEventListener('click', async () => {
853
- console.log('Logout button clicked');
854
  try {
855
  const response = await fetch('/logout', {
856
  method: 'POST',
@@ -858,42 +768,35 @@ if (logoutBtn) {
858
  });
859
  if (response.ok) {
860
  localStorage.removeItem('token');
861
- console.log('Token removed from localStorage');
862
  window.location.href = '/login';
863
  } else {
864
- console.error('Logout failed:', response.status);
865
  alert('Failed to log out. Please try again.');
866
  }
867
  } catch (error) {
868
- console.error('Logout error:', error);
869
  alert('Error during logout: ' + error.message);
870
  }
871
  });
872
  }
873
 
874
- // Settings Modal
875
  if (uiElements.settingsBtn) {
876
  uiElements.settingsBtn.addEventListener('click', async () => {
877
- console.log('Settings button clicked');
878
  if (!checkAuth()) {
879
- console.warn('Settings access blocked: No valid token');
880
- alert('Please log in to access settings.');
881
  return;
882
  }
883
  try {
884
  const response = await fetch('/api/settings', {
885
- headers: { 'Authorization': `Bearer ${checkAuth()}` }
886
  });
887
  if (!response.ok) {
888
  if (response.status === 401) {
889
- console.error('Unauthorized: Invalid or expired token');
890
  localStorage.removeItem('token');
891
  window.location.href = '/login';
892
  }
893
- throw new Error(`Failed to fetch settings: ${response.status}`);
894
  }
895
  const data = await response.json();
896
- console.log('Settings fetched:', data);
897
  document.getElementById('display_name').value = data.user_settings.display_name || '';
898
  document.getElementById('preferred_model').value = data.user_settings.preferred_model || 'standard';
899
  document.getElementById('job_title').value = data.user_settings.job_title || '';
@@ -923,8 +826,7 @@ if (uiElements.settingsBtn) {
923
  uiElements.settingsModal.classList.remove('hidden');
924
  toggleSidebar(false);
925
  } catch (err) {
926
- console.error('Error fetching settings:', err);
927
- alert('Failed to load settings: ' + err.message);
928
  }
929
  });
930
  }
@@ -939,7 +841,6 @@ if (uiElements.settingsForm) {
939
  uiElements.settingsForm.addEventListener('submit', (e) => {
940
  e.preventDefault();
941
  if (!checkAuth()) {
942
- alert('Please log in to save settings.');
943
  window.location.href = '/login';
944
  return;
945
  }
@@ -949,7 +850,7 @@ if (uiElements.settingsForm) {
949
  method: 'PUT',
950
  headers: {
951
  'Content-Type': 'application/json',
952
- 'Authorization': `Bearer ${checkAuth()}`
953
  },
954
  body: JSON.stringify(data)
955
  })
@@ -969,7 +870,6 @@ if (uiElements.settingsForm) {
969
  toggleSidebar(false);
970
  })
971
  .catch(err => {
972
- console.error('Error updating settings:', err);
973
  alert('Error updating settings: ' + err.message);
974
  });
975
  });
@@ -1011,64 +911,12 @@ if (uiElements.audioInput) uiElements.audioInput.addEventListener('change', prev
1011
 
1012
  if (uiElements.sendBtn) {
1013
  uiElements.sendBtn.addEventListener('click', (e) => {
1014
- e.preventDefault();
1015
- console.log('Send button clicked', {
1016
- disabled: uiElements.sendBtn.disabled,
1017
- input: uiElements.input?.value.trim(),
1018
- isRequestActive,
1019
- isRecording
1020
- });
1021
- if (uiElements.sendBtn.disabled || isRequestActive || isRecording) {
1022
- console.warn('Send button click ignored: disabled or request/recording active');
1023
- return;
1024
- }
1025
- submitMessage();
1026
- });
1027
-
1028
- let pressTimer;
1029
- uiElements.sendBtn.addEventListener('mousedown', (e) => {
1030
- if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
1031
- pressTimer = setTimeout(() => {
1032
- startVoiceRecording();
1033
- }, 500);
1034
- });
1035
- uiElements.sendBtn.addEventListener('mouseup', () => {
1036
- clearTimeout(pressTimer);
1037
- if (isRecording) stopVoiceRecording();
1038
- });
1039
- uiElements.sendBtn.addEventListener('mouseleave', () => {
1040
- clearTimeout(pressTimer);
1041
- if (isRecording) stopVoiceRecording();
1042
- });
1043
-
1044
- uiElements.sendBtn.addEventListener('touchstart', (e) => {
1045
  e.preventDefault();
1046
  if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
1047
- pressTimer = setTimeout(() => {
1048
- startVoiceRecording();
1049
- }, 500);
1050
- });
1051
- uiElements.sendBtn.addEventListener('touchend', (e) => {
1052
- e.preventDefault();
1053
- clearTimeout(pressTimer);
1054
- if (isRecording) stopVoiceRecording();
1055
- });
1056
- uiElements.sendBtn.addEventListener('touchcancel', (e) => {
1057
- e.preventDefault();
1058
- clearTimeout(pressTimer);
1059
- if (isRecording) stopVoiceRecording();
1060
- });
1061
- }
1062
-
1063
- if (!uiElements.sendBtn || !uiElements.input) {
1064
- console.error('Critical UI elements missing:', {
1065
- sendBtn: !!uiElements.sendBtn,
1066
- input: !!uiElements.input
1067
  });
1068
- alert('Error: Required UI elements are missing. Please reload the page.');
1069
- return;
1070
- }
1071
-
1072
 
1073
  let pressTimer;
1074
  uiElements.sendBtn.addEventListener('mousedown', (e) => {
@@ -1108,21 +956,25 @@ if (!uiElements.sendBtn || !uiElements.input) {
1108
  if (uiElements.form) {
1109
  uiElements.form.addEventListener('submit', (e) => {
1110
  e.preventDefault();
1111
- if (!isRecording && uiElements.input.value.trim()) {
1112
  submitMessage();
1113
  }
1114
  });
1115
  }
1116
 
1117
  if (uiElements.input) {
 
1118
  uiElements.input.addEventListener('input', () => {
1119
- autoResizeTextarea();
1120
- updateSendButtonState();
 
 
 
1121
  });
1122
  uiElements.input.addEventListener('keydown', (e) => {
1123
- if (e.key === 'Enter' && !e.shiftKey && !uiElements.sendBtn.disabled && !isRequestActive && !isRecording) {
1124
  e.preventDefault();
1125
- submitMessage();
1126
  }
1127
  });
1128
  }
@@ -1151,62 +1003,9 @@ if (uiElements.sidebarToggle) {
1151
  }
1152
 
1153
  if (uiElements.newConversationBtn) {
1154
- uiElements.newConversationBtn.addEventListener('click', async () => {
1155
- console.log('New conversation button clicked');
1156
- if (!checkAuth()) {
1157
- console.warn('New conversation blocked: No valid token');
1158
- alert('Please log in to create a new conversation.');
1159
- return;
1160
- }
1161
- try {
1162
- const response = await fetch('/api/conversations', {
1163
- method: 'POST',
1164
- headers: {
1165
- 'Content-Type': 'application/json',
1166
- 'Authorization': `Bearer ${checkAuth()}`
1167
- },
1168
- body: JSON.stringify({ title: 'New Conversation' })
1169
- });
1170
- if (!response.ok) {
1171
- if (response.status === 401) {
1172
- console.error('Unauthorized: Invalid or expired token');
1173
- localStorage.removeItem('token');
1174
- window.location.href = '/login';
1175
- }
1176
- throw new Error(`Failed to create conversation: ${response.status}`);
1177
- }
1178
- const data = await response.json();
1179
- console.log('New conversation created:', data);
1180
- currentConversationId = data.conversation_id;
1181
- currentConversationTitle = data.title;
1182
- conversationHistory = [];
1183
- sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
1184
- if (uiElements.chatBox) uiElements.chatBox.innerHTML = '';
1185
- if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
1186
- history.pushState(null, '', `/chat/${currentConversationId}`);
1187
- enterChatView();
1188
- await loadConversations();
1189
- toggleSidebar(false);
1190
- if (uiElements.chatBox) uiElements.chatBox.scrollTo({
1191
- top: uiElements.chatBox.scrollHeight,
1192
- behavior: 'smooth',
1193
- });
1194
- } catch (error) {
1195
- console.error('Error creating conversation:', error);
1196
- alert('Failed to create new conversation: ' + error.message);
1197
- }
1198
- });
1199
  }
1200
 
1201
-
1202
-
1203
- // Debug localStorage
1204
- const originalRemoveItem = localStorage.removeItem;
1205
- localStorage.removeItem = function (key) {
1206
- console.log('Removing from localStorage:', key);
1207
- originalRemoveItem.apply(this, arguments);
1208
- };
1209
-
1210
  // Offline mode detection
1211
  window.addEventListener('offline', () => {
1212
  if (uiElements.messageLimitWarning) {
 
50
 
51
  // Initialize AOS and load initial conversation
52
  document.addEventListener('DOMContentLoaded', async () => {
 
 
 
 
 
 
 
 
 
 
53
  AOS.init({
54
  duration: 800,
55
  easing: 'ease-out-cubic',
 
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
  }, 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
 
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
 
 
142
  });
143
  wrapper.querySelectorAll('hr').forEach(h => h.classList.add('styled-hr'));
144
  Prism.highlightAllUnder(wrapper);
 
145
  if (uiElements.chatBox) {
146
  uiElements.chatBox.scrollTo({
147
  top: uiElements.chatBox.scrollHeight,
 
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
  }
 
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
  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');
 
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
  }
354
  return response;
355
  } catch (error) {
 
356
  if (error.name === 'AbortError') {
357
  throw new Error('Request was aborted');
358
  }
 
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;
 
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
  });
437
  }
438
  } catch (error) {
 
439
  alert('Failed to load conversations. Please try again.');
440
  }
441
  }
 
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';
 
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
  }
 
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';
 
485
  }
486
  await loadConversations();
487
  } catch (error) {
 
488
  alert('Failed to delete conversation. Please try again.');
489
  }
490
  }
 
496
  method: 'POST',
497
  headers: {
498
  'Content-Type': 'application/json',
499
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
500
  },
501
  body: JSON.stringify({ role, content })
502
  });
 
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
  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
  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({
 
555
  method: 'PUT',
556
  headers: {
557
  'Content-Type': 'application/json',
558
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
559
  },
560
  body: JSON.stringify({ title: newTitle })
561
  });
 
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 {
605
  toggleSidebar(true);
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
  });
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
  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
  temperature: 0.7,
660
  max_new_tokens: 128000,
661
  enable_browsing: true,
662
+ output_format: 'text'
 
663
  };
664
  headers['Content-Type'] = 'application/json';
665
  }
 
678
  abortController = new AbortController();
679
 
680
  try {
 
681
  const response = await sendRequest(endpoint, payload ? JSON.stringify(payload) : formData, headers);
682
  let responseText = '';
683
  if (endpoint === '/api/audio-transcription') {
 
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;
 
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
  });
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) {
 
794
  localStorage.removeItem('token');
795
  window.location.href = '/login';
796
  }
797
+ throw new Error('Failed to fetch settings');
798
  }
799
  const data = await response.json();
 
800
  document.getElementById('display_name').value = data.user_settings.display_name || '';
801
  document.getElementById('preferred_model').value = data.user_settings.preferred_model || 'standard';
802
  document.getElementById('job_title').value = data.user_settings.job_title || '';
 
826
  uiElements.settingsModal.classList.remove('hidden');
827
  toggleSidebar(false);
828
  } catch (err) {
829
+ alert('Failed to load settings. Please try again.');
 
830
  }
831
  });
832
  }
 
841
  uiElements.settingsForm.addEventListener('submit', (e) => {
842
  e.preventDefault();
843
  if (!checkAuth()) {
 
844
  window.location.href = '/login';
845
  return;
846
  }
 
850
  method: 'PUT',
851
  headers: {
852
  'Content-Type': 'application/json',
853
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
854
  },
855
  body: JSON.stringify(data)
856
  })
 
870
  toggleSidebar(false);
871
  })
872
  .catch(err => {
 
873
  alert('Error updating settings: ' + err.message);
874
  });
875
  });
 
911
 
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;
922
  uiElements.sendBtn.addEventListener('mousedown', (e) => {
 
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) {
976
  e.preventDefault();
977
+ if (!isRecording && !uiElements.sendBtn.disabled) submitMessage();
978
  }
979
  });
980
  }
 
1003
  }
1004
 
1005
  if (uiElements.newConversationBtn) {
1006
+ uiElements.newConversationBtn.addEventListener('click', createNewConversation);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1007
  }
1008
 
 
 
 
 
 
 
 
 
 
1009
  // Offline mode detection
1010
  window.addEventListener('offline', () => {
1011
  if (uiElements.messageLimitWarning) {
templates/chat.html CHANGED
@@ -3,30 +3,44 @@
3
 
4
  <head>
5
  <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
7
- <meta name="description" content="Chat with MGZon Chatbot, an AI-powered tool for coding, analysis, and e-commerce queries. Supports text, image, and audio inputs." />
8
- <meta name="keywords" content="MGZon Chatbot, AI chatbot, code generation, DeepSeek, Gradio, FastAPI, e-commerce, programming, Mark Al-Asfar" />
 
 
 
9
  <meta name="author" content="Mark Al-Asfar" />
10
  <meta name="robots" content="index, follow" />
11
  <title>MGZon Chatbot – AI Assistant</title>
12
  <link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />
13
  <link rel="icon" type="image/x-icon" href="/static/favicon.ico" />
14
  <link rel="apple-touch-icon" href="/static/images/mg.svg" />
 
 
 
15
  <link rel="manifest" href="/static/manifest.json">
 
 
16
  <link rel="apple-touch-icon" sizes="180x180" href="/static/images/icons/mg-180.png">
17
  <meta name="apple-mobile-web-app-capable" content="yes">
18
  <meta name="apple-mobile-web-app-title" content="MGZon">
19
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
 
 
20
  <meta name="theme-color" content="#2d3748">
21
- <meta property="og:title" content="MGZon Chatbot – AI Assistant" />
22
- <meta property="og:description" content="Chat with MGZon Chatbot for coding, analysis, and e-commerce queries with text, image, and audio support." />
 
23
  <meta property="og:image" content="/static/images/mg.svg" />
24
  <meta property="og:url" content="https://mgzon-mgzon-app.hf.space/chat" />
25
  <meta property="og:type" content="website" />
 
26
  <meta name="twitter:card" content="summary_large_image" />
27
  <meta name="twitter:title" content="MGZon Chatbot – AI Assistant" />
28
- <meta name="twitter:description" content="Chat with MGZon Chatbot for coding, analysis, and e-commerce queries with text, image, and audio support." />
 
29
  <meta name="twitter:image" content="/static/images/mg.svg" />
 
30
  <script type="application/ld+json">
31
  {
32
  "@context": "https://schema.org",
@@ -36,16 +50,29 @@
36
  "description": "An AI-powered chatbot for coding, analysis, and e-commerce queries."
37
  }
38
  </script>
 
39
  <link rel="preconnect" href="https://fonts.googleapis.com" />
40
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet" />
 
 
41
  <script src="https://cdn.tailwindcss.com"></script>
 
42
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
43
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" />
44
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
45
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
 
46
  <script src="https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos.js"></script>
47
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos.css" />
 
 
 
 
 
 
 
48
  <script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8/hammer.min.js"></script>
 
49
  <link rel="stylesheet" href="/static/css/animation/style.css" />
50
  <link rel="stylesheet" href="/static/css/button.css" />
51
  <link rel="stylesheet" href="/static/css/chat/bubble.css" />
@@ -68,11 +95,13 @@
68
  <link rel="stylesheet" href="/static/css/style.css" />
69
  <link rel="stylesheet" href="/static/css/webkit.css" />
70
  <link rel="stylesheet" href="/static/css/sidebar.css" />
 
71
  <link rel="preload" href="/static/js/chat.js" as="script">
72
  <link rel="preload" href="/static/css/chat/style.css" as="style">
73
  <link rel="preload" href="/static/images/mg.svg" as="image">
74
- <link rel="preload" href="/static/images/splash-screen.png" as="image">
75
  </head>
 
76
  <body class="min-h-screen flex flex-col bg-gradient-to-br from-gray-900 via-teal-900 to-emerald-900">
77
  <!-- Sidebar -->
78
  <aside id="sidebar"
 
3
 
4
  <head>
5
  <meta charset="UTF-8">
6
+ <meta name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
8
+ <meta name="description"
9
+ content="Chat with MGZon Chatbot, an AI-powered tool for coding, analysis, and e-commerce queries. Supports text, image, and audio inputs." />
10
+ <meta name="keywords"
11
+ content="MGZon Chatbot, AI chatbot, code generation, DeepSeek, Gradio, FastAPI, e-commerce, programming, Mark Al-Asfar" />
12
  <meta name="author" content="Mark Al-Asfar" />
13
  <meta name="robots" content="index, follow" />
14
  <title>MGZon Chatbot – AI Assistant</title>
15
  <link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />
16
  <link rel="icon" type="image/x-icon" href="/static/favicon.ico" />
17
  <link rel="apple-touch-icon" href="/static/images/mg.svg" />
18
+ <!-- Open Graph -->
19
+ <meta property="og:title" content="MGZon Chatbot – AI Assistant" />
20
+ <!-- manifest for Android/Chrome -->
21
  <link rel="manifest" href="/static/manifest.json">
22
+
23
+ <!-- iOS Web App Support -->
24
  <link rel="apple-touch-icon" sizes="180x180" href="/static/images/icons/mg-180.png">
25
  <meta name="apple-mobile-web-app-capable" content="yes">
26
  <meta name="apple-mobile-web-app-title" content="MGZon">
27
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
28
+
29
+ <!-- General Theme -->
30
  <meta name="theme-color" content="#2d3748">
31
+
32
+ <meta property="og:description"
33
+ content="Chat with MGZon Chatbot for coding, analysis, and e-commerce queries with text, image, and audio support." />
34
  <meta property="og:image" content="/static/images/mg.svg" />
35
  <meta property="og:url" content="https://mgzon-mgzon-app.hf.space/chat" />
36
  <meta property="og:type" content="website" />
37
+ <!-- Twitter Card -->
38
  <meta name="twitter:card" content="summary_large_image" />
39
  <meta name="twitter:title" content="MGZon Chatbot – AI Assistant" />
40
+ <meta name="twitter:description"
41
+ content="Chat with MGZon Chatbot for coding, analysis, and e-commerce queries with text, image, and audio support." />
42
  <meta name="twitter:image" content="/static/images/mg.svg" />
43
+ <!-- JSON-LD -->
44
  <script type="application/ld+json">
45
  {
46
  "@context": "https://schema.org",
 
50
  "description": "An AI-powered chatbot for coding, analysis, and e-commerce queries."
51
  }
52
  </script>
53
+ <!-- Fonts & Styles -->
54
  <link rel="preconnect" href="https://fonts.googleapis.com" />
55
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
56
+ rel="stylesheet" />
57
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
58
  <script src="https://cdn.tailwindcss.com"></script>
59
+ <!-- Markdown & Syntax Highlight -->
60
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
61
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" />
62
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
63
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
64
+ <!-- Animations -->
65
  <script src="https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos.js"></script>
66
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos.css" />
67
+ <!-- Carousel -->
68
+ <script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js"></script>
69
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide.min.css" />
70
+ <!-- Smooth Scroll -->
71
+ <script src="https://cdn.jsdelivr.net/npm/locomotive-scroll@4.1.4/dist/locomotive-scroll.min.js"></script>
72
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/locomotive-scroll@4.1.4/dist/locomotive-scroll.min.css" />
73
+ <!-- Hammer.js for touch gestures -->
74
  <script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8/hammer.min.js"></script>
75
+ <!-- Project-Specific Styles -->
76
  <link rel="stylesheet" href="/static/css/animation/style.css" />
77
  <link rel="stylesheet" href="/static/css/button.css" />
78
  <link rel="stylesheet" href="/static/css/chat/bubble.css" />
 
95
  <link rel="stylesheet" href="/static/css/style.css" />
96
  <link rel="stylesheet" href="/static/css/webkit.css" />
97
  <link rel="stylesheet" href="/static/css/sidebar.css" />
98
+ <!-- Preload Resources -->
99
  <link rel="preload" href="/static/js/chat.js" as="script">
100
  <link rel="preload" href="/static/css/chat/style.css" as="style">
101
  <link rel="preload" href="/static/images/mg.svg" as="image">
102
+ <link rel="preload" href="https://raw.githubusercontent.com/4gels/icons/refs/heads/main/splash-screen.png" as="image">
103
  </head>
104
+
105
  <body class="min-h-screen flex flex-col bg-gradient-to-br from-gray-900 via-teal-900 to-emerald-900">
106
  <!-- Sidebar -->
107
  <aside id="sidebar"