ibrahimlasfar commited on
Commit
611e18a
·
1 Parent(s): b51ab8b

Fix footer position, prompts size, flexible textarea, send button, prompts refresh, and add voice recording

Browse files
Files changed (3) hide show
  1. static/css/chat/style.css +4 -1
  2. static/js/chat.js +324 -156
  3. templates/chat.html +1 -1
static/css/chat/style.css CHANGED
@@ -16,6 +16,9 @@
16
  text-overflow: ellipsis;
17
  }
18
 
 
 
 
19
  .chat-controls {
20
  display: flex;
21
  align-items: center;
@@ -55,4 +58,4 @@
55
  overflow-wrap: break-word;
56
  word-wrap: break-word;
57
  word-break: break-word;
58
- }
 
16
  text-overflow: ellipsis;
17
  }
18
 
19
+ html {
20
+ -webkit-text-size-adjust: 100%;
21
+ }
22
  .chat-controls {
23
  display: flex;
24
  align-items: center;
 
58
  overflow-wrap: break-word;
59
  word-wrap: break-word;
60
  word-break: break-word;
61
+ }
static/js/chat.js CHANGED
@@ -36,12 +36,13 @@ let abortController = null;
36
  let mediaRecorder = null;
37
  let audioChunks = [];
38
  let isRecording = false;
39
- let pressTimer = null;
40
 
41
  // Auto-resize textarea
42
  function autoResizeTextarea() {
43
- input.style.height = 'auto';
44
- input.style.height = `${Math.min(input.scrollHeight, 200)}px`; // Max height is 200px as per CSS
 
 
45
  }
46
 
47
  // تحميل المحادثة عند تحميل الصفحة
@@ -61,7 +62,9 @@ document.addEventListener('DOMContentLoaded', () => {
61
  // Initialize textarea height
62
  autoResizeTextarea();
63
  // Enable send button based on input
64
- btn.disabled = input.value.trim() === '' && fileInput.files.length === 0 && audioInput.files.length === 0;
 
 
65
  });
66
 
67
  // تحقق من الـ token
@@ -106,20 +109,36 @@ function renderMarkdown(el) {
106
 
107
  // Chat view.
108
  function enterChatView() {
109
- mainHeader.style.display = 'none';
110
- chatHeader.style.display = 'flex';
111
- chatHeader.setAttribute('aria-hidden', 'false');
112
- chatBox.style.display = 'flex';
113
- initialContent.style.display = 'none';
 
 
 
 
 
 
 
 
114
  }
115
 
116
  // Home view.
117
  function leaveChatView() {
118
- mainHeader.style.display = 'flex';
119
- chatHeader.style.display = 'none';
120
- chatHeader.setAttribute('aria-hidden', 'true');
121
- chatBox.style.display = 'none';
122
- initialContent.style.display = 'flex';
 
 
 
 
 
 
 
 
123
  }
124
 
125
  // Chat bubble.
@@ -128,9 +147,11 @@ function addMsg(who, text) {
128
  div.className = 'bubble ' + (who === 'user' ? 'bubble-user' : 'bubble-assist');
129
  div.dataset.text = text;
130
  renderMarkdown(div);
131
- chatBox.appendChild(div);
132
- chatBox.style.display = 'flex';
133
- chatBox.scrollTop = chatBox.scrollHeight;
 
 
134
  return div;
135
  }
136
 
@@ -145,42 +166,70 @@ function clearAllMessages() {
145
  if (loadingEl) loadingEl.remove();
146
  streamMsg = null;
147
  }
148
- chatBox.innerHTML = '';
149
- input.value = '';
150
- btn.disabled = true;
151
- stopBtn.style.display = 'none';
152
- btn.style.display = 'inline-flex';
153
- filePreview.style.display = 'none';
154
- audioPreview.style.display = 'none';
155
- messageLimitWarning.classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  enterChatView();
157
  autoResizeTextarea();
158
  }
159
 
160
  // File preview.
161
  function previewFile() {
162
- if (fileInput.files.length > 0) {
163
  const file = fileInput.files[0];
164
  if (file.type.startsWith('image/')) {
165
  const reader = new FileReader();
166
  reader.onload = e => {
167
- filePreview.innerHTML = `<img src="${e.target.result}" class="upload-preview">`;
168
- filePreview.style.display = 'block';
169
- audioPreview.style.display = 'none';
170
- btn.disabled = false; // Enable send button when file is selected
 
 
 
 
 
 
171
  };
172
  reader.readAsDataURL(file);
173
  }
174
  }
175
- if (audioInput.files.length > 0) {
176
  const file = audioInput.files[0];
177
  if (file.type.startsWith('audio/')) {
178
  const reader = new FileReader();
179
  reader.onload = e => {
180
- audioPreview.innerHTML = `<audio controls src="${e.target.result}"></audio>`;
181
- audioPreview.style.display = 'block';
182
- filePreview.style.display = 'none';
183
- btn.disabled = false; // Enable send button when audio is selected
 
 
 
 
 
 
184
  };
185
  reader.readAsDataURL(file);
186
  }
@@ -191,7 +240,9 @@ function previewFile() {
191
  function startVoiceRecording() {
192
  if (isRequestActive || isRecording) return;
193
  isRecording = true;
194
- btn.classList.add('recording');
 
 
195
  navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
196
  mediaRecorder = new MediaRecorder(stream);
197
  audioChunks = [];
@@ -203,14 +254,18 @@ function startVoiceRecording() {
203
  console.error('Error accessing microphone:', err);
204
  alert('Failed to access microphone. Please check permissions.');
205
  isRecording = false;
206
- btn.classList.remove('recording');
 
 
207
  });
208
  }
209
 
210
  function stopVoiceRecording() {
211
  if (mediaRecorder && mediaRecorder.state === 'recording') {
212
  mediaRecorder.stop();
213
- btn.classList.remove('recording');
 
 
214
  isRecording = false;
215
  mediaRecorder.addEventListener('stop', async () => {
216
  const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
@@ -231,12 +286,24 @@ async function submitAudioMessage(formData) {
231
  const loadingEl = document.createElement('span');
232
  loadingEl.className = 'loading';
233
  streamMsg.appendChild(loadingEl);
234
- stopBtn.style.display = 'inline-flex';
235
- btn.style.display = 'none';
236
- input.value = '';
237
- btn.disabled = true;
238
- filePreview.style.display = 'none';
239
- audioPreview.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
  isRequestActive = true;
242
  abortController = new AbortController();
@@ -253,15 +320,25 @@ async function submitAudioMessage(formData) {
253
 
254
  if (!response.ok) {
255
  if (response.status === 403) {
256
- messageLimitWarning.classList.remove('hidden');
257
- input.disabled = true;
258
- const loadingEl = streamMsg.querySelector('.loading');
259
- if (loadingEl) loadingEl.remove();
260
- streamMsg = null;
 
 
 
 
 
 
261
  isRequestActive = false;
262
  abortController = null;
263
- btn.style.display = 'inline-flex';
264
- stopBtn.style.display = 'none';
 
 
 
 
265
  window.location.href = '/login';
266
  return;
267
  }
@@ -275,17 +352,23 @@ async function submitAudioMessage(formData) {
275
 
276
  const data = await response.json();
277
  const transcription = data.transcription || 'Error: No transcription generated.';
278
- streamMsg.dataset.text = transcription;
279
- renderMarkdown(streamMsg);
280
- streamMsg.dataset.done = '1';
 
 
281
  conversationHistory.push({ role: 'assistant', content: transcription });
282
  sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
283
 
284
  streamMsg = null;
285
  isRequestActive = false;
286
  abortController = null;
287
- btn.style.display = 'inline-flex';
288
- stopBtn.style.display = 'none';
 
 
 
 
289
  } catch (error) {
290
  if (streamMsg) {
291
  const loadingEl = streamMsg.querySelector('.loading');
@@ -297,8 +380,12 @@ async function submitAudioMessage(formData) {
297
  isRequestActive = false;
298
  abortController = null;
299
  }
300
- btn.style.display = 'inline-flex';
301
- stopBtn.style.display = 'none';
 
 
 
 
302
  }
303
  }
304
 
@@ -311,7 +398,7 @@ async function submitMessage() {
311
  let inputType = 'text';
312
  let outputFormat = 'text';
313
 
314
- if (fileInput.files.length > 0) {
315
  const file = fileInput.files[0];
316
  if (file.type.startsWith('image/')) {
317
  endpoint = '/api/image-analysis';
@@ -320,7 +407,7 @@ async function submitMessage() {
320
  formData.append('file', file);
321
  formData.append('output_format', 'text');
322
  }
323
- } else if (audioInput.files.length > 0) {
324
  const file = audioInput.files[0];
325
  if (file.type.startsWith('audio/')) {
326
  endpoint = '/api/audio-transcription';
@@ -348,12 +435,24 @@ async function submitMessage() {
348
  const loadingEl = document.createElement('span');
349
  loadingEl.className = 'loading';
350
  streamMsg.appendChild(loadingEl);
351
- stopBtn.style.display = 'inline-flex';
352
- btn.style.display = 'none';
353
- input.value = '';
354
- btn.disabled = true;
355
- filePreview.style.display = 'none';
356
- audioPreview.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
357
  autoResizeTextarea();
358
 
359
  isRequestActive = true;
@@ -371,15 +470,25 @@ async function submitMessage() {
371
 
372
  if (!response.ok) {
373
  if (response.status === 403) {
374
- messageLimitWarning.classList.remove('hidden');
375
- input.disabled = true;
376
- const loadingEl = streamMsg.querySelector('.loading');
377
- if (loadingEl) loadingEl.remove();
378
- streamMsg = null;
 
 
 
 
 
 
379
  isRequestActive = false;
380
  abortController = null;
381
- btn.style.display = 'inline-flex';
382
- stopBtn.style.display = 'none';
 
 
 
 
383
  window.location.href = '/login';
384
  return;
385
  }
@@ -394,17 +503,21 @@ async function submitMessage() {
394
  if (endpoint === '/api/audio-transcription') {
395
  const data = await response.json();
396
  const transcription = data.transcription || 'Error: No transcription generated.';
397
- streamMsg.dataset.text = transcription;
398
- renderMarkdown(streamMsg);
399
- streamMsg.dataset.done = '1';
 
 
400
  conversationHistory.push({ role: 'assistant', content: transcription });
401
  sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
402
  } else if (endpoint === '/api/image-analysis') {
403
  const data = await response.json();
404
  const analysis = data.image_analysis || 'Error: No analysis generated.';
405
- streamMsg.dataset.text = analysis;
406
- renderMarkdown(streamMsg);
407
- streamMsg.dataset.done = '1';
 
 
408
  conversationHistory.push({ role: 'assistant', content: analysis });
409
  sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
410
  } else {
@@ -416,14 +529,20 @@ async function submitMessage() {
416
  const { done, value } = await reader.read();
417
  if (done) break;
418
  buffer += decoder.decode(value, { stream: true });
419
- streamMsg.dataset.text = buffer;
420
- currentAssistantText = buffer;
421
- const loadingEl = streamMsg.querySelector('.loading');
422
- if (loadingEl) loadingEl.remove();
423
- renderMarkdown(streamMsg);
424
- chatBox.scrollTop = chatBox.scrollHeight;
 
 
 
 
 
 
 
425
  }
426
- streamMsg.dataset.done = '1';
427
  conversationHistory.push({ role: 'assistant', content: buffer });
428
  sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
429
  }
@@ -431,8 +550,12 @@ async function submitMessage() {
431
  streamMsg = null;
432
  isRequestActive = false;
433
  abortController = null;
434
- btn.style.display = 'inline-flex';
435
- stopBtn.style.display = 'none';
 
 
 
 
436
  } catch (error) {
437
  if (streamMsg) {
438
  const loadingEl = streamMsg.querySelector('.loading');
@@ -444,8 +567,12 @@ async function submitMessage() {
444
  isRequestActive = false;
445
  abortController = null;
446
  }
447
- btn.style.display = 'inline-flex';
448
- stopBtn.style.display = 'none';
 
 
 
 
449
  }
450
  }
451
 
@@ -468,99 +595,140 @@ function stopStream(forceCancel = false) {
468
  streamMsg.dataset.done = '1';
469
  streamMsg = null;
470
  }
471
- stopBtn.style.display = 'none';
472
- btn.style.display = 'inline-flex';
473
- stopBtn.style.pointerEvents = 'auto';
 
 
 
 
 
 
474
  }
475
 
476
  // Prompts.
477
  promptItems.forEach(p => {
478
  p.addEventListener('click', e => {
479
  e.preventDefault();
480
- input.value = p.dataset.prompt;
481
- autoResizeTextarea();
482
- btn.disabled = false;
 
 
 
 
483
  submitMessage();
484
  });
485
  });
486
 
487
  // File and audio inputs.
488
- fileBtn.addEventListener('click', () => {
489
- fileInput.click();
490
- });
491
- audioBtn.addEventListener('click', () => {
492
- audioInput.click();
493
- });
494
- fileInput.addEventListener('change', previewFile);
495
- audioInput.addEventListener('change', previewFile);
 
 
 
 
 
 
 
 
 
 
 
 
496
 
497
  // Send button events for send and voice recording.
498
- btn.addEventListener('mousedown', e => {
499
- if (btn.disabled || isRequestActive) return;
500
- pressTimer = setTimeout(() => {
501
  startVoiceRecording();
502
- }, 500); // 500ms for long press
503
- });
504
- btn.addEventListener('mouseup', e => {
505
- clearTimeout(pressTimer);
506
- if (isRecording) {
507
- stopVoiceRecording();
508
- }
509
- });
510
- btn.addEventListener('touchstart', e => {
511
- e.preventDefault();
512
- if (btn.disabled || isRequestActive) return;
513
- pressTimer = setTimeout(() => {
 
 
514
  startVoiceRecording();
515
- }, 500);
516
- });
517
- btn.addEventListener('touchend', e => {
518
- e.preventDefault();
519
- clearTimeout(pressTimer);
520
- if (isRecording) {
521
- stopVoiceRecording();
522
- }
523
- });
 
 
 
 
 
524
 
525
  // Submit.
526
- form.addEventListener('submit', e => {
527
- e.preventDefault();
528
- if (!isRecording) {
529
- submitMessage();
530
- }
531
- });
532
-
533
- // Handle Enter key to submit without adding new line.
534
- input.addEventListener('keydown', e => {
535
- if (e.key === 'Enter' && !e.shiftKey) {
536
  e.preventDefault();
537
- if (!isRecording && !btn.disabled) {
538
  submitMessage();
539
  }
540
- }
541
- });
542
 
543
- // Auto-resize textarea on input.
544
- input.addEventListener('input', () => {
545
- autoResizeTextarea();
546
- btn.disabled = input.value.trim() === '' && fileInput.files.length === 0 && audioInput.files.length === 0;
547
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
 
549
  // Stop.
550
- stopBtn.addEventListener('click', () => {
551
- stopBtn.style.pointerEvents = 'none';
552
- stopStream();
553
- });
 
 
554
 
555
  // Home.
556
- homeBtn.addEventListener('click', () => {
557
- window.location.href = '/';
558
- });
 
 
559
 
560
  // Clear messages.
561
- clearBtn.addEventListener('click', () => {
562
- clearAllMessages();
563
- });
 
 
564
 
565
  // Login button.
566
  if (loginBtn) {
 
36
  let mediaRecorder = null;
37
  let audioChunks = [];
38
  let isRecording = false;
 
39
 
40
  // Auto-resize textarea
41
  function autoResizeTextarea() {
42
+ if (input) {
43
+ input.style.height = 'auto';
44
+ input.style.height = `${Math.min(input.scrollHeight, 200)}px`; // Max height is 200px as per CSS
45
+ }
46
  }
47
 
48
  // تحميل المحادثة عند تحميل الصفحة
 
62
  // Initialize textarea height
63
  autoResizeTextarea();
64
  // Enable send button based on input
65
+ if (btn && input && fileInput && audioInput) {
66
+ btn.disabled = input.value.trim() === '' && fileInput.files.length === 0 && audioInput.files.length === 0;
67
+ }
68
  });
69
 
70
  // تحقق من الـ token
 
109
 
110
  // Chat view.
111
  function enterChatView() {
112
+ if (mainHeader) {
113
+ mainHeader.style.display = 'none';
114
+ }
115
+ if (chatHeader) {
116
+ chatHeader.style.display = 'flex';
117
+ chatHeader.setAttribute('aria-hidden', 'false');
118
+ }
119
+ if (chatBox) {
120
+ chatBox.style.display = 'flex';
121
+ }
122
+ if (initialContent) {
123
+ initialContent.style.display = 'none';
124
+ }
125
  }
126
 
127
  // Home view.
128
  function leaveChatView() {
129
+ if (mainHeader) {
130
+ mainHeader.style.display = 'flex';
131
+ }
132
+ if (chatHeader) {
133
+ chatHeader.style.display = 'none';
134
+ chatHeader.setAttribute('aria-hidden', 'true');
135
+ }
136
+ if (chatBox) {
137
+ chatBox.style.display = 'none';
138
+ }
139
+ if (initialContent) {
140
+ initialContent.style.display = 'flex';
141
+ }
142
  }
143
 
144
  // Chat bubble.
 
147
  div.className = 'bubble ' + (who === 'user' ? 'bubble-user' : 'bubble-assist');
148
  div.dataset.text = text;
149
  renderMarkdown(div);
150
+ if (chatBox) {
151
+ chatBox.appendChild(div);
152
+ chatBox.style.display = 'flex';
153
+ chatBox.scrollTop = chatBox.scrollHeight;
154
+ }
155
  return div;
156
  }
157
 
 
166
  if (loadingEl) loadingEl.remove();
167
  streamMsg = null;
168
  }
169
+ if (chatBox) {
170
+ chatBox.innerHTML = '';
171
+ }
172
+ if (input) {
173
+ input.value = '';
174
+ }
175
+ if (btn) {
176
+ btn.disabled = true;
177
+ }
178
+ if (stopBtn) {
179
+ stopBtn.style.display = 'none';
180
+ }
181
+ if (btn) {
182
+ btn.style.display = 'inline-flex';
183
+ }
184
+ if (filePreview) {
185
+ filePreview.style.display = 'none';
186
+ }
187
+ if (audioPreview) {
188
+ audioPreview.style.display = 'none';
189
+ }
190
+ if (messageLimitWarning) {
191
+ messageLimitWarning.classList.add('hidden');
192
+ }
193
  enterChatView();
194
  autoResizeTextarea();
195
  }
196
 
197
  // File preview.
198
  function previewFile() {
199
+ if (fileInput && fileInput.files.length > 0) {
200
  const file = fileInput.files[0];
201
  if (file.type.startsWith('image/')) {
202
  const reader = new FileReader();
203
  reader.onload = e => {
204
+ if (filePreview) {
205
+ filePreview.innerHTML = `<img src="${e.target.result}" class="upload-preview">`;
206
+ filePreview.style.display = 'block';
207
+ }
208
+ if (audioPreview) {
209
+ audioPreview.style.display = 'none';
210
+ }
211
+ if (btn) {
212
+ btn.disabled = false; // Enable send button when file is selected
213
+ }
214
  };
215
  reader.readAsDataURL(file);
216
  }
217
  }
218
+ if (audioInput && audioInput.files.length > 0) {
219
  const file = audioInput.files[0];
220
  if (file.type.startsWith('audio/')) {
221
  const reader = new FileReader();
222
  reader.onload = e => {
223
+ if (audioPreview) {
224
+ audioPreview.innerHTML = `<audio controls src="${e.target.result}"></audio>`;
225
+ audioPreview.style.display = 'block';
226
+ }
227
+ if (filePreview) {
228
+ filePreview.style.display = 'none';
229
+ }
230
+ if (btn) {
231
+ btn.disabled = false; // Enable send button when audio is selected
232
+ }
233
  };
234
  reader.readAsDataURL(file);
235
  }
 
240
  function startVoiceRecording() {
241
  if (isRequestActive || isRecording) return;
242
  isRecording = true;
243
+ if (btn) {
244
+ btn.classList.add('recording');
245
+ }
246
  navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
247
  mediaRecorder = new MediaRecorder(stream);
248
  audioChunks = [];
 
254
  console.error('Error accessing microphone:', err);
255
  alert('Failed to access microphone. Please check permissions.');
256
  isRecording = false;
257
+ if (btn) {
258
+ btn.classList.remove('recording');
259
+ }
260
  });
261
  }
262
 
263
  function stopVoiceRecording() {
264
  if (mediaRecorder && mediaRecorder.state === 'recording') {
265
  mediaRecorder.stop();
266
+ if (btn) {
267
+ btn.classList.remove('recording');
268
+ }
269
  isRecording = false;
270
  mediaRecorder.addEventListener('stop', async () => {
271
  const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
 
286
  const loadingEl = document.createElement('span');
287
  loadingEl.className = 'loading';
288
  streamMsg.appendChild(loadingEl);
289
+ if (stopBtn) {
290
+ stopBtn.style.display = 'inline-flex';
291
+ }
292
+ if (btn) {
293
+ btn.style.display = 'none';
294
+ }
295
+ if (input) {
296
+ input.value = '';
297
+ }
298
+ if (btn) {
299
+ btn.disabled = true;
300
+ }
301
+ if (filePreview) {
302
+ filePreview.style.display = 'none';
303
+ }
304
+ if (audioPreview) {
305
+ audioPreview.style.display = 'none';
306
+ }
307
 
308
  isRequestActive = true;
309
  abortController = new AbortController();
 
320
 
321
  if (!response.ok) {
322
  if (response.status === 403) {
323
+ if (messageLimitWarning) {
324
+ messageLimitWarning.classList.remove('hidden');
325
+ }
326
+ if (input) {
327
+ input.disabled = true;
328
+ }
329
+ if (streamMsg) {
330
+ const loadingEl = streamMsg.querySelector('.loading');
331
+ if (loadingEl) loadingEl.remove();
332
+ streamMsg = null;
333
+ }
334
  isRequestActive = false;
335
  abortController = null;
336
+ if (btn) {
337
+ btn.style.display = 'inline-flex';
338
+ }
339
+ if (stopBtn) {
340
+ stopBtn.style.display = 'none';
341
+ }
342
  window.location.href = '/login';
343
  return;
344
  }
 
352
 
353
  const data = await response.json();
354
  const transcription = data.transcription || 'Error: No transcription generated.';
355
+ if (streamMsg) {
356
+ streamMsg.dataset.text = transcription;
357
+ renderMarkdown(streamMsg);
358
+ streamMsg.dataset.done = '1';
359
+ }
360
  conversationHistory.push({ role: 'assistant', content: transcription });
361
  sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
362
 
363
  streamMsg = null;
364
  isRequestActive = false;
365
  abortController = null;
366
+ if (btn) {
367
+ btn.style.display = 'inline-flex';
368
+ }
369
+ if (stopBtn) {
370
+ stopBtn.style.display = 'none';
371
+ }
372
  } catch (error) {
373
  if (streamMsg) {
374
  const loadingEl = streamMsg.querySelector('.loading');
 
380
  isRequestActive = false;
381
  abortController = null;
382
  }
383
+ if (btn) {
384
+ btn.style.display = 'inline-flex';
385
+ }
386
+ if (stopBtn) {
387
+ stopBtn.style.display = 'none';
388
+ }
389
  }
390
  }
391
 
 
398
  let inputType = 'text';
399
  let outputFormat = 'text';
400
 
401
+ if (fileInput && fileInput.files.length > 0) {
402
  const file = fileInput.files[0];
403
  if (file.type.startsWith('image/')) {
404
  endpoint = '/api/image-analysis';
 
407
  formData.append('file', file);
408
  formData.append('output_format', 'text');
409
  }
410
+ } else if (audioInput && audioInput.files.length > 0) {
411
  const file = audioInput.files[0];
412
  if (file.type.startsWith('audio/')) {
413
  endpoint = '/api/audio-transcription';
 
435
  const loadingEl = document.createElement('span');
436
  loadingEl.className = 'loading';
437
  streamMsg.appendChild(loadingEl);
438
+ if (stopBtn) {
439
+ stopBtn.style.display = 'inline-flex';
440
+ }
441
+ if (btn) {
442
+ btn.style.display = 'none';
443
+ }
444
+ if (input) {
445
+ input.value = '';
446
+ }
447
+ if (btn) {
448
+ btn.disabled = true;
449
+ }
450
+ if (filePreview) {
451
+ filePreview.style.display = 'none';
452
+ }
453
+ if (audioPreview) {
454
+ audioPreview.style.display = 'none';
455
+ }
456
  autoResizeTextarea();
457
 
458
  isRequestActive = true;
 
470
 
471
  if (!response.ok) {
472
  if (response.status === 403) {
473
+ if (messageLimitWarning) {
474
+ messageLimitWarning.classList.remove('hidden');
475
+ }
476
+ if (input) {
477
+ input.disabled = true;
478
+ }
479
+ if (streamMsg) {
480
+ const loadingEl = streamMsg.querySelector('.loading');
481
+ if (loadingEl) loadingEl.remove();
482
+ streamMsg = null;
483
+ }
484
  isRequestActive = false;
485
  abortController = null;
486
+ if (btn) {
487
+ btn.style.display = 'inline-flex';
488
+ }
489
+ if (stopBtn) {
490
+ stopBtn.style.display = 'none';
491
+ }
492
  window.location.href = '/login';
493
  return;
494
  }
 
503
  if (endpoint === '/api/audio-transcription') {
504
  const data = await response.json();
505
  const transcription = data.transcription || 'Error: No transcription generated.';
506
+ if (streamMsg) {
507
+ streamMsg.dataset.text = transcription;
508
+ renderMarkdown(streamMsg);
509
+ streamMsg.dataset.done = '1';
510
+ }
511
  conversationHistory.push({ role: 'assistant', content: transcription });
512
  sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
513
  } else if (endpoint === '/api/image-analysis') {
514
  const data = await response.json();
515
  const analysis = data.image_analysis || 'Error: No analysis generated.';
516
+ if (streamMsg) {
517
+ streamMsg.dataset.text = analysis;
518
+ renderMarkdown(streamMsg);
519
+ streamMsg.dataset.done = '1';
520
+ }
521
  conversationHistory.push({ role: 'assistant', content: analysis });
522
  sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
523
  } else {
 
529
  const { done, value } = await reader.read();
530
  if (done) break;
531
  buffer += decoder.decode(value, { stream: true });
532
+ if (streamMsg) {
533
+ streamMsg.dataset.text = buffer;
534
+ currentAssistantText = buffer;
535
+ const loadingEl = streamMsg.querySelector('.loading');
536
+ if (loadingEl) loadingEl.remove();
537
+ renderMarkdown(streamMsg);
538
+ if (chatBox) {
539
+ chatBox.scrollTop = chatBox.scrollHeight;
540
+ }
541
+ }
542
+ }
543
+ if (streamMsg) {
544
+ streamMsg.dataset.done = '1';
545
  }
 
546
  conversationHistory.push({ role: 'assistant', content: buffer });
547
  sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
548
  }
 
550
  streamMsg = null;
551
  isRequestActive = false;
552
  abortController = null;
553
+ if (btn) {
554
+ btn.style.display = 'inline-flex';
555
+ }
556
+ if (stopBtn) {
557
+ stopBtn.style.display = 'none';
558
+ }
559
  } catch (error) {
560
  if (streamMsg) {
561
  const loadingEl = streamMsg.querySelector('.loading');
 
567
  isRequestActive = false;
568
  abortController = null;
569
  }
570
+ if (btn) {
571
+ btn.style.display = 'inline-flex';
572
+ }
573
+ if (stopBtn) {
574
+ stopBtn.style.display = 'none';
575
+ }
576
  }
577
  }
578
 
 
595
  streamMsg.dataset.done = '1';
596
  streamMsg = null;
597
  }
598
+ if (stopBtn) {
599
+ stopBtn.style.display = 'none';
600
+ }
601
+ if (btn) {
602
+ btn.style.display = 'inline-flex';
603
+ }
604
+ if (stopBtn) {
605
+ stopBtn.style.pointerEvents = 'auto';
606
+ }
607
  }
608
 
609
  // Prompts.
610
  promptItems.forEach(p => {
611
  p.addEventListener('click', e => {
612
  e.preventDefault();
613
+ if (input) {
614
+ input.value = p.dataset.prompt;
615
+ autoResizeTextarea();
616
+ }
617
+ if (btn) {
618
+ btn.disabled = false;
619
+ }
620
  submitMessage();
621
  });
622
  });
623
 
624
  // File and audio inputs.
625
+ if (fileBtn) {
626
+ fileBtn.addEventListener('click', () => {
627
+ if (fileInput) {
628
+ fileInput.click();
629
+ }
630
+ });
631
+ }
632
+ if (audioBtn) {
633
+ audioBtn.addEventListener('click', () => {
634
+ if (audioInput) {
635
+ audioInput.click();
636
+ }
637
+ });
638
+ }
639
+ if (fileInput) {
640
+ fileInput.addEventListener('change', previewFile);
641
+ }
642
+ if (audioInput) {
643
+ audioInput.addEventListener('change', previewFile);
644
+ }
645
 
646
  // Send button events for send and voice recording.
647
+ if (btn) {
648
+ btn.addEventListener('mousedown', e => {
649
+ if (btn.disabled || isRequestActive || isRecording) return;
650
  startVoiceRecording();
651
+ });
652
+ btn.addEventListener('mouseup', e => {
653
+ if (isRecording) {
654
+ stopVoiceRecording();
655
+ }
656
+ });
657
+ btn.addEventListener('mouseleave', e => {
658
+ if (isRecording) {
659
+ stopVoiceRecording();
660
+ }
661
+ });
662
+ btn.addEventListener('touchstart', e => {
663
+ e.preventDefault();
664
+ if (btn.disabled || isRequestActive || isRecording) return;
665
  startVoiceRecording();
666
+ });
667
+ btn.addEventListener('touchend', e => {
668
+ e.preventDefault();
669
+ if (isRecording) {
670
+ stopVoiceRecording();
671
+ }
672
+ });
673
+ btn.addEventListener('touchcancel', e => {
674
+ e.preventDefault();
675
+ if (isRecording) {
676
+ stopVoiceRecording();
677
+ }
678
+ });
679
+ }
680
 
681
  // Submit.
682
+ if (form) {
683
+ form.addEventListener('submit', e => {
 
 
 
 
 
 
 
 
684
  e.preventDefault();
685
+ if (!isRecording) {
686
  submitMessage();
687
  }
688
+ });
689
+ }
690
 
691
+ // Handle Enter key to submit without adding new line.
692
+ if (input) {
693
+ input.addEventListener('keydown', e => {
694
+ if (e.key === 'Enter' && !e.shiftKey) {
695
+ e.preventDefault();
696
+ if (!isRecording && !btn.disabled) {
697
+ submitMessage();
698
+ }
699
+ }
700
+ });
701
+
702
+ // Auto-resize textarea on input.
703
+ input.addEventListener('input', () => {
704
+ autoResizeTextarea();
705
+ if (btn && input && fileInput && audioInput) {
706
+ btn.disabled = input.value.trim() === '' && fileInput.files.length === 0 && audioInput.files.length === 0;
707
+ }
708
+ });
709
+ }
710
 
711
  // Stop.
712
+ if (stopBtn) {
713
+ stopBtn.addEventListener('click', () => {
714
+ stopBtn.style.pointerEvents = 'none';
715
+ stopStream();
716
+ });
717
+ }
718
 
719
  // Home.
720
+ if (homeBtn) {
721
+ homeBtn.addEventListener('click', () => {
722
+ window.location.href = '/';
723
+ });
724
+ }
725
 
726
  // Clear messages.
727
+ if (clearBtn) {
728
+ clearBtn.addEventListener('click', () => {
729
+ clearAllMessages();
730
+ });
731
+ }
732
 
733
  // Login button.
734
  if (loginBtn) {
templates/chat.html CHANGED
@@ -77,7 +77,7 @@
77
  </head>
78
  <body class="min-h-screen flex flex-col bg-gradient-to-br from-gray-900 via-teal-900 to-emerald-900">
79
  <!-- Main Header -->
80
- <header class="main-header p-4 flex justify-between items-center">
81
  <div class="logo flex items-center">
82
  <img src="/static/images/mg.svg" alt="MGZon Logo" class="w-16 h-16 mr-2 animate-pulse" />
83
  <h1 class="text-2xl font-bold text-white">MGZon Chatbot</h1>
 
77
  </head>
78
  <body class="min-h-screen flex flex-col bg-gradient-to-br from-gray-900 via-teal-900 to-emerald-900">
79
  <!-- Main Header -->
80
+ <header id="mainHeader" class="main-header p-4 flex justify-between items-center">
81
  <div class="logo flex items-center">
82
  <img src="/static/images/mg.svg" alt="MGZon Logo" class="w-16 h-16 mr-2 animate-pulse" />
83
  <h1 class="text-2xl font-bold text-white">MGZon Chatbot</h1>