ibrahimlasfar commited on
Commit
b51ab8b
·
1 Parent(s): 4fa0b30

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

Browse files
static/css/button.css CHANGED
@@ -3,7 +3,7 @@
3
  * SPDX-License-Identifier: Apache-2.0
4
  */
5
 
6
- #sendBtn, #stopBtn, #voiceBtn, #fileBtn, #audioBtn {
7
  width: 2.75rem;
8
  height: 2.75rem;
9
  border: none;
@@ -22,12 +22,17 @@
22
  color: white;
23
  }
24
 
25
- #sendBtn:hover:not(:disabled) {
 
 
 
 
 
26
  transform: translateY(-2px) scale(1.05);
27
  box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
28
  }
29
 
30
- #sendBtn:active:not(:disabled) {
31
  transform: translateY(0) scale(0.98);
32
  }
33
 
@@ -53,25 +58,6 @@
53
  transform: translateY(0) scale(0.98);
54
  }
55
 
56
- #voiceBtn {
57
- background: linear-gradient(135deg, #10b981, #059669);
58
- color: white;
59
- }
60
-
61
- #voiceBtn.recording {
62
- background: linear-gradient(135deg, #f87171, #ef4444);
63
- transform: scale(1.1);
64
- }
65
-
66
- #voiceBtn:hover:not(.recording) {
67
- transform: translateY(-2px) scale(1.05);
68
- box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
69
- }
70
-
71
- #voiceBtn:active:not(.recording) {
72
- transform: translateY(0) scale(0.98);
73
- }
74
-
75
  #fileBtn, #audioBtn {
76
  background: linear-gradient(135deg, #6b7280, #4b5563);
77
  color: white;
@@ -93,6 +79,18 @@
93
  transition: transform 0.2s ease;
94
  }
95
 
96
- #sendBtn:hover:not(:disabled) #sendIcon {
97
  transform: translateX(2px);
98
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  * SPDX-License-Identifier: Apache-2.0
4
  */
5
 
6
+ #sendBtn, #stopBtn, #fileBtn, #audioBtn {
7
  width: 2.75rem;
8
  height: 2.75rem;
9
  border: none;
 
22
  color: white;
23
  }
24
 
25
+ #sendBtn.recording {
26
+ background: linear-gradient(135deg, #f87171, #ef4444);
27
+ transform: scale(1.1);
28
+ }
29
+
30
+ #sendBtn:hover:not(:disabled):not(.recording) {
31
  transform: translateY(-2px) scale(1.05);
32
  box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
33
  }
34
 
35
+ #sendBtn:active:not(:disabled):not(.recording) {
36
  transform: translateY(0) scale(0.98);
37
  }
38
 
 
58
  transform: translateY(0) scale(0.98);
59
  }
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  #fileBtn, #audioBtn {
62
  background: linear-gradient(135deg, #6b7280, #4b5563);
63
  color: white;
 
79
  transition: transform 0.2s ease;
80
  }
81
 
82
+ #sendBtn:hover:not(:disabled):not(.recording) #sendIcon {
83
  transform: translateX(2px);
84
  }
85
+
86
+ #sendBtn.recording #sendIcon {
87
+ display: none;
88
+ }
89
+
90
+ #sendBtn.recording::after {
91
+ content: '';
92
+ width: 1rem;
93
+ height: 1rem;
94
+ background: url('/static/images/mic.svg') no-repeat center;
95
+ background-size: contain;
96
+ }
static/css/input.css CHANGED
@@ -5,15 +5,15 @@
5
 
6
  #inputContainer {
7
  display: flex;
8
- align-items: center;
9
  flex: 1;
10
  background: linear-gradient(145deg, #1a1a1a, #141414);
11
  border-radius: 1rem;
12
  border: 1px solid rgba(255, 255, 255, 0.1);
13
- padding: 0.25rem;
14
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
15
  max-width: 800px;
16
- margin: 0 auto;
17
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
18
  position: relative;
19
  }
@@ -38,24 +38,29 @@
38
  word-break: break-word;
39
  overflow-wrap: break-word;
40
  min-height: 40px;
41
- max-height: 120px;
42
- resize: vertical;
43
  font-family: 'Inter', sans-serif;
 
 
44
  }
45
 
46
  #rightIconGroup {
47
  position: absolute;
48
  right: 0.5rem;
 
49
  display: flex;
50
  gap: 0.5rem;
 
51
  }
52
 
53
  @media (max-width: 768px) {
54
  #inputContainer {
55
  max-width: 100%;
56
- padding: 0.25rem 0.5rem;
57
  }
58
  #userInput {
59
  padding: 0.5rem 2.5rem 0.5rem 0.5rem;
 
60
  }
61
  }
 
5
 
6
  #inputContainer {
7
  display: flex;
8
+ align-items: flex-end; /* Align items to the bottom for better textarea expansion */
9
  flex: 1;
10
  background: linear-gradient(145deg, #1a1a1a, #141414);
11
  border-radius: 1rem;
12
  border: 1px solid rgba(255, 255, 255, 0.1);
13
+ padding: 0.5rem;
14
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
15
  max-width: 800px;
16
+ margin: 0 auto 1rem auto; /* Reduced bottom margin */
17
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
18
  position: relative;
19
  }
 
38
  word-break: break-word;
39
  overflow-wrap: break-word;
40
  min-height: 40px;
41
+ max-height: 200px; /* Increased max-height for more expansion */
42
+ resize: none; /* Disable manual resize */
43
  font-family: 'Inter', sans-serif;
44
+ line-height: 1.5;
45
+ overflow-y: auto;
46
  }
47
 
48
  #rightIconGroup {
49
  position: absolute;
50
  right: 0.5rem;
51
+ bottom: 0.5rem;
52
  display: flex;
53
  gap: 0.5rem;
54
+ align-items: center;
55
  }
56
 
57
  @media (max-width: 768px) {
58
  #inputContainer {
59
  max-width: 100%;
60
+ padding: 0.5rem;
61
  }
62
  #userInput {
63
  padding: 0.5rem 2.5rem 0.5rem 0.5rem;
64
+ font-size: 0.9rem;
65
  }
66
  }
static/images/mic.svg ADDED
static/js/chat.js CHANGED
@@ -15,7 +15,6 @@ const btn = document.getElementById('sendBtn');
15
  const stopBtn = document.getElementById('stopBtn');
16
  const fileBtn = document.getElementById('fileBtn');
17
  const audioBtn = document.getElementById('audioBtn');
18
- const voiceBtn = document.getElementById('voiceBtn');
19
  const fileInput = document.getElementById('fileInput');
20
  const audioInput = document.getElementById('audioInput');
21
  const filePreview = document.getElementById('filePreview');
@@ -36,6 +35,14 @@ let isRequestActive = false;
36
  let abortController = null;
37
  let mediaRecorder = null;
38
  let audioChunks = [];
 
 
 
 
 
 
 
 
39
 
40
  // تحميل المحادثة عند تحميل الصفحة
41
  document.addEventListener('DOMContentLoaded', () => {
@@ -51,6 +58,10 @@ document.addEventListener('DOMContentLoaded', () => {
51
  addMsg(msg.role, msg.content);
52
  });
53
  }
 
 
 
 
54
  });
55
 
56
  // تحقق من الـ token
@@ -143,6 +154,7 @@ function clearAllMessages() {
143
  audioPreview.style.display = 'none';
144
  messageLimitWarning.classList.add('hidden');
145
  enterChatView();
 
146
  }
147
 
148
  // File preview.
@@ -155,6 +167,7 @@ function previewFile() {
155
  filePreview.innerHTML = `<img src="${e.target.result}" class="upload-preview">`;
156
  filePreview.style.display = 'block';
157
  audioPreview.style.display = 'none';
 
158
  };
159
  reader.readAsDataURL(file);
160
  }
@@ -167,6 +180,7 @@ function previewFile() {
167
  audioPreview.innerHTML = `<audio controls src="${e.target.result}"></audio>`;
168
  audioPreview.style.display = 'block';
169
  filePreview.style.display = 'none';
 
170
  };
171
  reader.readAsDataURL(file);
172
  }
@@ -175,24 +189,29 @@ function previewFile() {
175
 
176
  // Voice recording.
177
  function startVoiceRecording() {
 
 
 
178
  navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
179
  mediaRecorder = new MediaRecorder(stream);
180
  audioChunks = [];
181
  mediaRecorder.start();
182
- voiceBtn.classList.add('recording');
183
  mediaRecorder.addEventListener('dataavailable', event => {
184
  audioChunks.push(event.data);
185
  });
186
  }).catch(err => {
187
  console.error('Error accessing microphone:', err);
188
  alert('Failed to access microphone. Please check permissions.');
 
 
189
  });
190
  }
191
 
192
  function stopVoiceRecording() {
193
  if (mediaRecorder && mediaRecorder.state === 'recording') {
194
  mediaRecorder.stop();
195
- voiceBtn.classList.remove('recording');
 
196
  mediaRecorder.addEventListener('stop', async () => {
197
  const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
198
  const formData = new FormData();
@@ -285,7 +304,7 @@ async function submitAudioMessage(formData) {
285
 
286
  // Send user message.
287
  async function submitMessage() {
288
- if (isRequestActive) return;
289
  let message = input.value.trim();
290
  let formData = new FormData();
291
  let endpoint = '/api/chat';
@@ -335,6 +354,7 @@ async function submitMessage() {
335
  btn.disabled = true;
336
  filePreview.style.display = 'none';
337
  audioPreview.style.display = 'none';
 
338
 
339
  isRequestActive = true;
340
  abortController = new AbortController();
@@ -431,7 +451,10 @@ async function submitMessage() {
431
 
432
  // Stop streaming and cancel the ongoing request.
433
  function stopStream(forceCancel = false) {
434
- if (!isRequestActive) return;
 
 
 
435
  isRequestActive = false;
436
  if (abortController) {
437
  abortController.abort();
@@ -455,6 +478,8 @@ promptItems.forEach(p => {
455
  p.addEventListener('click', e => {
456
  e.preventDefault();
457
  input.value = p.dataset.prompt;
 
 
458
  submitMessage();
459
  });
460
  });
@@ -469,32 +494,58 @@ audioBtn.addEventListener('click', () => {
469
  fileInput.addEventListener('change', previewFile);
470
  audioInput.addEventListener('change', previewFile);
471
 
472
- // Voice recording events.
473
- voiceBtn.addEventListener('mousedown', startVoiceRecording);
474
- voiceBtn.addEventListener('touchstart', e => {
 
 
 
 
 
 
 
 
 
 
 
475
  e.preventDefault();
476
- startVoiceRecording();
 
 
 
477
  });
478
- voiceBtn.addEventListener('mouseup', stopVoiceRecording);
479
- voiceBtn.addEventListener('touchend', e => {
480
  e.preventDefault();
481
- stopVoiceRecording();
 
 
 
482
  });
483
 
484
  // Submit.
485
  form.addEventListener('submit', e => {
486
  e.preventDefault();
487
- submitMessage();
 
 
488
  });
489
 
490
  // Handle Enter key to submit without adding new line.
491
  input.addEventListener('keydown', e => {
492
  if (e.key === 'Enter' && !e.shiftKey) {
493
  e.preventDefault();
494
- submitMessage();
 
 
495
  }
496
  });
497
 
 
 
 
 
 
 
498
  // Stop.
499
  stopBtn.addEventListener('click', () => {
500
  stopBtn.style.pointerEvents = 'none';
@@ -517,8 +568,3 @@ if (loginBtn) {
517
  window.location.href = '/login';
518
  });
519
  }
520
-
521
- // Enable send button only if input has text or files.
522
- input.addEventListener('input', () => {
523
- btn.disabled = input.value.trim() === '' && fileInput.files.length === 0 && audioInput.files.length === 0;
524
- });
 
15
  const stopBtn = document.getElementById('stopBtn');
16
  const fileBtn = document.getElementById('fileBtn');
17
  const audioBtn = document.getElementById('audioBtn');
 
18
  const fileInput = document.getElementById('fileInput');
19
  const audioInput = document.getElementById('audioInput');
20
  const filePreview = document.getElementById('filePreview');
 
35
  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
  // تحميل المحادثة عند تحميل الصفحة
48
  document.addEventListener('DOMContentLoaded', () => {
 
58
  addMsg(msg.role, msg.content);
59
  });
60
  }
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
 
154
  audioPreview.style.display = 'none';
155
  messageLimitWarning.classList.add('hidden');
156
  enterChatView();
157
+ autoResizeTextarea();
158
  }
159
 
160
  // File preview.
 
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
  }
 
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
  }
 
189
 
190
  // Voice recording.
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 = [];
198
  mediaRecorder.start();
 
199
  mediaRecorder.addEventListener('dataavailable', event => {
200
  audioChunks.push(event.data);
201
  });
202
  }).catch(err => {
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' });
217
  const formData = new FormData();
 
304
 
305
  // Send user message.
306
  async function submitMessage() {
307
+ if (isRequestActive || isRecording) return;
308
  let message = input.value.trim();
309
  let formData = new FormData();
310
  let endpoint = '/api/chat';
 
354
  btn.disabled = true;
355
  filePreview.style.display = 'none';
356
  audioPreview.style.display = 'none';
357
+ autoResizeTextarea();
358
 
359
  isRequestActive = true;
360
  abortController = new AbortController();
 
451
 
452
  // Stop streaming and cancel the ongoing request.
453
  function stopStream(forceCancel = false) {
454
+ if (!isRequestActive && !isRecording) return;
455
+ if (isRecording) {
456
+ stopVoiceRecording();
457
+ }
458
  isRequestActive = false;
459
  if (abortController) {
460
  abortController.abort();
 
478
  p.addEventListener('click', e => {
479
  e.preventDefault();
480
  input.value = p.dataset.prompt;
481
+ autoResizeTextarea();
482
+ btn.disabled = false;
483
  submitMessage();
484
  });
485
  });
 
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';
 
568
  window.location.href = '/login';
569
  });
570
  }
 
 
 
 
 
templates/chat.html CHANGED
@@ -119,7 +119,7 @@
119
  </div>
120
  <p class="system text-gray-300 mb-4">
121
  A versatile chatbot powered by DeepSeek, GPT-OSS, CLIP, Whisper, and TTS.<br>
122
- Type your query, upload images/files, or record audio!
123
  </p>
124
  <!-- Prompts -->
125
  <div class="prompts w-full max-w-md mx-auto grid gap-2">
@@ -176,14 +176,7 @@
176
  </svg>
177
  </button>
178
  <input type="file" id="audioInput" accept="audio/*" style="display: none;" />
179
- <button type="button" id="voiceBtn" class="icon-btn" aria-label="Record Voice" title="Hold to Record Voice">
180
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
181
- <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
182
- <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
183
- <path d="M12 19v4" />
184
- </svg>
185
- </button>
186
- <button type="submit" id="sendBtn" class="icon-btn" disabled aria-label="Send" title="Send">
187
  <svg id="sendIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
188
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7-7 7M3 12h11" />
189
  </svg>
 
119
  </div>
120
  <p class="system text-gray-300 mb-4">
121
  A versatile chatbot powered by DeepSeek, GPT-OSS, CLIP, Whisper, and TTS.<br>
122
+ Type your query, upload images/files, or hold the send button to record audio!
123
  </p>
124
  <!-- Prompts -->
125
  <div class="prompts w-full max-w-md mx-auto grid gap-2">
 
176
  </svg>
177
  </button>
178
  <input type="file" id="audioInput" accept="audio/*" style="display: none;" />
179
+ <button type="submit" id="sendBtn" class="icon-btn" disabled aria-label="Send or Hold to Record" title="Click to Send or Hold to Record Voice">
 
 
 
 
 
 
 
180
  <svg id="sendIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
181
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7-7 7M3 12h11" />
182
  </svg>