Athspi commited on
Commit
062884e
·
verified ·
1 Parent(s): 3dfb66f

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +346 -233
static/index.html CHANGED
@@ -1,10 +1,9 @@
1
- <!-- static/index.html -->
2
  <!DOCTYPE html>
3
  <html lang="en">
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">
7
- <title>AI Chat with Gemini</title>
8
  <style>
9
  :root {
10
  --background-color: #212121;
@@ -20,9 +19,10 @@
20
  --code-text-color: #f8f8f2;
21
  --link-color: #64b5f6;
22
  --quote-color: #4CAF50;
 
 
23
  }
24
 
25
- /* Reset and Base Styles */
26
  * {
27
  box-sizing: border-box;
28
  margin: 0;
@@ -31,44 +31,40 @@
31
 
32
  html, body {
33
  height: 100%;
34
- overflow: hidden; /* Prevent scrolling on main body */
 
35
  }
36
 
37
  body {
38
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
39
  background-color: var(--background-color);
40
  color: var(--text-color);
41
  display: flex;
42
  flex-direction: column;
43
- height: 100%;
44
  line-height: 1.6;
45
  }
46
 
47
  .chat-container {
48
  display: flex;
49
  flex-direction: column;
50
- height: 100%;
 
 
51
  width: 100%;
52
- max-width: 800px; /* Max width for larger screens */
53
- margin: 0 auto; /* Center the container */
54
- border-left: 1px solid var(--border-color); /* Optional: add subtle borders */
55
- border-right: 1px solid var(--border-color);
56
  }
57
 
58
  .chat-area {
59
- flex-grow: 1; /* Takes remaining vertical space */
60
- overflow-y: auto; /* Enable scrolling for chat messages */
61
  padding: 20px 16px;
62
- display: flex;
63
- flex-direction: column;
64
  }
65
 
66
- /* Welcome Screen */
67
  .welcome-screen {
68
  text-align: center;
69
- margin: auto; /* Center content vertically and horizontally */
70
  color: var(--placeholder-color);
71
  padding: 20px;
 
72
  }
73
 
74
  .welcome-screen h2 {
@@ -76,54 +72,91 @@
76
  font-weight: 400;
77
  margin-bottom: 10px;
78
  }
79
-
80
  .welcome-screen p {
81
  font-size: 1rem;
82
  line-height: 1.5;
 
83
  }
84
 
85
  .welcome-screen.hidden {
86
- display: none; /* Hide when chat starts */
87
  }
88
 
89
- /* Message Bubbles */
90
  .message {
91
- max-width: 85%; /* Limit bubble width */
92
  padding: 12px 16px;
93
  border-radius: 18px;
94
  margin-bottom: 12px;
95
- word-wrap: break-word; /* Ensure long words wrap */
96
  line-height: 1.6;
97
  font-size: 1rem;
98
- position: relative; /* For audio button positioning */
99
- animation: fadeIn 0.3s ease-out; /* Fade in animation for new messages */
 
 
 
 
 
 
 
100
  }
101
 
102
  .user-message {
103
  background-color: var(--user-bubble-color);
104
- align-self: flex-end; /* Align user messages to the right */
105
- border-bottom-right-radius: 4px; /* Slight variation for visual appeal */
106
  }
107
 
108
  .ai-message {
109
  background-color: var(--ai-bubble-color);
110
- align-self: flex-start; /* Align AI messages to the left */
111
- border-bottom-left-radius: 4px; /* Slight variation for visual appeal */
112
- padding-bottom: 30px; /* Space for audio button at the bottom */
113
  }
114
 
115
- /* AI Message Content Styling (for markdown-converted HTML) */
116
  .ai-message-content {
117
- overflow: hidden; /* Contains floats/margins */
118
- }
119
- .ai-message-content p { margin-bottom: 12px; }
120
- .ai-message-content p:last-child { margin-bottom: 0; } /* No bottom margin on last paragraph */
121
- .ai-message-content strong { color: #ffffff; font-weight: 600; }
122
- .ai-message-content em { color: #d1d1d1; font-style: italic; }
123
- .ai-message-content a { color: var(--link-color); text-decoration: none; word-break: break-all; }
124
- .ai-message-content a:hover { text-decoration: underline; }
125
- .ai-message-content ul, .ai-message-content ol { padding-left: 24px; margin-bottom: 12px; }
126
- .ai-message-content li { margin-bottom: 6px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  .ai-message-content blockquote {
128
  border-left: 3px solid var(--quote-color);
129
  padding-left: 12px;
@@ -131,14 +164,15 @@
131
  color: #bdbdbd;
132
  margin-bottom: 12px;
133
  }
134
- /* Code Blocks */
135
  .ai-message-content pre {
136
  background-color: var(--code-bg-color);
137
  border-radius: 6px;
138
  padding: 12px;
139
- overflow-x: auto; /* Enable horizontal scrolling for long code lines */
140
  margin-bottom: 12px;
141
  }
 
142
  .ai-message-content code {
143
  font-family: 'Courier New', Courier, monospace;
144
  background-color: var(--code-bg-color);
@@ -146,21 +180,20 @@
146
  border-radius: 3px;
147
  color: var(--code-text-color);
148
  font-size: 0.9em;
149
- white-space: pre-wrap; /* Preserve whitespace and wrap long lines */
150
  }
151
- /* Inline code styling specific for the `code-block` class which is on `pre` */
152
  .ai-message-content .code-block code {
153
- padding: 0; /* Code inside pre shouldn't have extra padding */
154
- background-color: transparent; /* No background for code inside pre */
155
- color: inherit; /* Inherit color from pre */
156
  }
157
 
158
- /* Audio Button Styling */
159
  .audio-button {
160
  position: absolute;
161
  bottom: 6px;
162
  right: 8px;
163
- background: rgba(255, 255, 255, 0.1); /* Semi-transparent background */
164
  border: none;
165
  border-radius: 50%;
166
  width: 30px;
@@ -169,95 +202,114 @@
169
  align-items: center;
170
  justify-content: center;
171
  cursor: pointer;
172
- transition: background-color 0.2s;
173
- z-index: 10; /* Ensure it's above message content */
174
  }
 
175
  .audio-button:hover {
176
- background: rgba(255, 255, 255, 0.2);
 
177
  }
178
- .audio-button svg {
179
- width: 18px;
180
- height: 18px;
181
- fill: var(--icon-color);
182
- transition: fill 0.2s;
183
  }
184
- .audio-button:hover svg {
185
- fill: white; /* Icon brighter on hover */
 
186
  }
187
- /* Loading spinner for audio button */
188
  .audio-button.loading .speaker-icon {
189
- display: none; /* Hide speaker icon when loading */
190
  }
 
191
  .audio-button .loader {
192
- display: none; /* Hidden by default */
193
  width: 18px;
194
  height: 18px;
195
- border: 2px solid #f3f3f3; /* Light grey border */
196
- border-top: 2px solid var(--send-button-color); /* Green top border for spinner */
197
  border-radius: 50%;
198
- animation: spin 1s linear infinite; /* Spin animation */
199
  }
 
200
  .audio-button.loading .loader {
201
- display: block; /* Show spinner when loading */
202
  }
 
203
  @keyframes spin {
204
  0% { transform: rotate(0deg); }
205
  100% { transform: rotate(360deg); }
206
  }
207
 
208
- /* Typing Indicator */
209
  .typing-indicator {
210
  display: flex;
211
  align-items: center;
212
  padding: 12px 16px;
213
  }
 
214
  .typing-indicator span {
215
- height: 8px; width: 8px; background-color: var(--icon-color); border-radius: 50%; display: inline-block; margin: 0 2px;
 
 
 
 
 
216
  animation: bounce 1.4s infinite both;
217
  }
218
- .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
219
- .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
 
 
 
 
 
 
220
 
221
  @keyframes bounce {
222
  0%, 80%, 100% { transform: scale(0); }
223
  40% { transform: scale(1.0); }
224
  }
225
 
226
- /* Input Area */
227
  .input-area {
228
  display: flex;
229
- align-items: flex-end; /* Align items to the bottom (textarea can grow) */
230
  padding: 10px 16px;
231
  border-top: 1px solid var(--border-color);
232
  background-color: var(--input-area-color);
233
- flex-shrink: 0; /* Prevent shrinking */
234
  }
 
235
  .input-wrapper {
236
  display: flex;
237
  align-items: center;
238
  width: 100%;
239
  background-color: var(--user-bubble-color);
240
- border-radius: 24px; /* Pill-shaped input */
241
  padding: 4px;
242
  }
 
243
  .input-area textarea {
244
- flex-grow: 1; /* Takes up available space */
245
  border: none;
246
  background: transparent;
247
  color: var(--text-color);
248
- resize: none; /* Disable manual resize */
249
  font-size: 1rem;
250
  line-height: 1.5;
251
- max-height: 120px; /* Max height for textarea */
252
- overflow-y: auto; /* Scroll if content exceeds max height */
253
  padding: 8px 12px;
254
- font-family: inherit; /* Inherit font from body */
255
- outline: none; /* Remove focus outline */
 
 
 
 
256
  }
257
- .input-area textarea::placeholder { color: var(--placeholder-color); }
258
 
259
- /* Icon Buttons (Attach, Send) */
260
- .input-area .icon-button {
261
  background: none;
262
  border: none;
263
  padding: 8px;
@@ -267,51 +319,66 @@
267
  justify-content: center;
268
  transition: opacity 0.2s;
269
  }
270
- .input-area .icon-button:hover {
 
271
  opacity: 0.8;
272
  }
273
- .input-area .icon-button svg { width: 24px; height: 24px; fill: var(--icon-color); }
274
-
 
 
 
 
 
275
  .send-button {
276
  background-color: var(--send-button-color);
277
  border-radius: 50%;
278
  padding: 8px;
279
  transition: background-color 0.2s, opacity 0.2s;
280
  }
 
281
  .send-button.disabled {
282
  background-color: #555;
283
  cursor: not-allowed;
284
  opacity: 0.6;
285
  }
 
286
  .send-button.disabled svg {
287
  fill: #888;
288
  }
289
- .send-button svg { fill: white; width: 24px; height: 24px; }
290
 
291
- /* Animation for new messages */
292
- @keyframes fadeIn {
293
- from { opacity: 0; transform: translateY(10px); }
294
- to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
 
295
  }
296
  </style>
297
  </head>
298
  <body>
299
  <div class="chat-container">
300
- <main class="chat-area">
301
  <div class="welcome-screen">
302
  <h2>Welcome to AstroChat!</h2>
303
- <p>Hello! I'm your AI assistant powered by Gemini. Ask me anything - I can explain concepts, generate ideas, or help with coding!</p>
304
- <p>Try asking me something like: <em>"Explain quantum computing in simple terms"</em> or <em>"Help me debug this Python code"</em></p>
305
  </div>
306
  </main>
307
 
308
  <footer class="input-area">
309
  <form id="chat-form" class="input-wrapper">
310
- <button type="button" class="icon-button" id="attach-button">
311
  <svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .83-.67 1.5-1.5 1.5s-1.5-.67-1.5-1.5V6H10v9.5a2.5 2.5 0 0 0 5 0V5c-1.38 0-2.5 1.12-2.5 2.5v10.5c0 1.38-1.12 2.5-2.5 2.5s-2.5-1.12-2.5-2.5V5a4 4 0 0 1 8 0v11.5c-1.1 0-2-.9-2-2V6h-1.5z"></path></svg>
312
  </button>
313
- <textarea id="message-input" placeholder="Ask anything..." rows="1"></textarea>
314
- <button type="submit" id="send-button" class="icon-button send-button disabled">
315
  <svg viewBox="0 0 24 24"><path d="M2 21l21-9L2 3v7l15 2-15 2v7z"></path></svg>
316
  </button>
317
  </form>
@@ -322,213 +389,259 @@
322
  document.addEventListener('DOMContentLoaded', () => {
323
  const chatForm = document.getElementById('chat-form');
324
  const messageInput = document.getElementById('message-input');
325
- const chatArea = document.querySelector('.chat-area');
326
  const welcomeScreen = document.querySelector('.welcome-screen');
327
  const sendButton = document.getElementById('send-button');
328
  const attachButton = document.getElementById('attach-button');
329
 
330
- // Global variable to hold the currently playing audio object
331
- let currentAudio = null;
332
-
333
- // Adjusts the height of the textarea based on content
334
- const adjustTextareaHeight = () => {
335
- messageInput.style.height = 'auto'; // Reset height
336
- messageInput.style.height = `${messageInput.scrollHeight}px`; // Set to scroll height
337
- updateSendButtonState(); // Update send button state after height adjustment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  };
339
 
340
- // Updates the disabled state of the send button
341
- const updateSendButtonState = () => {
 
 
 
 
 
 
342
  const isDisabled = messageInput.value.trim() === '';
343
  sendButton.classList.toggle('disabled', isDisabled);
344
  sendButton.disabled = isDisabled;
345
- };
346
-
347
- // Scrolls the chat area to the bottom
348
- const scrollToBottom = () => {
349
  chatArea.scrollTop = chatArea.scrollHeight;
350
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
- // Adds a message to the chat area
353
- // `plainText` is used specifically for audio generation, keeping original text without HTML
354
- const addMessage = (content, sender, isHTML = false, plainText = '') => {
355
- welcomeScreen.classList.add('hidden'); // Hide welcome screen once chat begins
356
 
357
  const messageElement = document.createElement('div');
358
- messageElement.classList.add('message', `${sender}-message`);
359
 
360
  const contentDiv = document.createElement('div');
361
  if (isHTML) {
362
- contentDiv.classList.add(`${sender}-message-content`);
363
- contentDiv.innerHTML = content; // Set innerHTML for HTML content
364
  } else {
365
- contentDiv.textContent = content; // Set textContent for plain text
366
  }
 
367
  messageElement.appendChild(contentDiv);
368
-
369
  // Add audio button for AI messages
370
  if (sender === 'ai' && plainText) {
371
  const audioButton = document.createElement('button');
372
- audioButton.classList.add('audio-button');
373
- // Store the plain text directly on the button for easy retrieval
374
- audioButton.dataset.plainText = plainText;
375
  audioButton.innerHTML = `
376
  <svg class="speaker-icon" viewBox="0 0 24 24">
377
  <path d="M3 10v4c0 .55.45 1 1 1h3.5l5.5 5V5L7.5 9H4c-.55 0-1 .45-1 1zm14 0h-1.5c-2.31 0-4.22-1.74-4.47-4H10v12h1.5v-2.22c.25-2.26 2.16-4.22 4.47-4.22H17c1.1 0 2 .9 2 2s-.9 2-2 2h-1.5v2H17c2.21 0 4-1.79 4-4s-1.79-4-4-4z"/>
378
  </svg>
379
  <div class="loader"></div>
380
  `;
381
- audioButton.title = "Listen to response"; // Add tooltip
382
- audioButton.addEventListener('click', () => playAudio(audioButton, plainText));
 
 
 
383
  messageElement.appendChild(audioButton);
384
  }
385
 
386
  chatArea.appendChild(messageElement);
387
  scrollToBottom();
388
  return messageElement;
389
- };
390
-
391
- // Displays a typing indicator for the AI
392
- const showTypingIndicator = () => {
393
- const indicatorElement = document.createElement('div');
394
- indicatorElement.classList.add('message', 'ai-message', 'typing-indicator');
395
- indicatorElement.innerHTML = '<span></span><span></span><span></span>';
396
- chatArea.appendChild(indicatorElement);
397
- scrollToBottom();
398
- return indicatorElement; // Return element to remove it later
399
- };
400
-
401
- // Plays audio for a given text
402
- const playAudio = async (buttonElement, text) => {
403
- // Stop any currently playing audio
404
- if (currentAudio) {
405
- currentAudio.pause();
406
- currentAudio.currentTime = 0;
407
- currentAudio = null;
408
- }
409
-
410
- buttonElement.classList.add('loading'); // Show loading spinner
411
- buttonElement.disabled = true; // Disable button during loading
412
-
413
- try {
414
- const response = await fetch('/generate-audio', {
415
- method: 'POST',
416
- headers: { 'Content-Type': 'application/json' },
417
- body: JSON.stringify({ text: text })
418
- });
419
-
420
- const data = await response.json();
421
-
422
- if (data.error) {
423
- throw new Error(data.error);
424
- }
425
-
426
- currentAudio = new Audio(data.audio_url);
427
- currentAudio.play();
428
-
429
- // Remove loading state when audio ends
430
- currentAudio.onended = () => {
431
- buttonElement.classList.remove('loading');
432
- buttonElement.disabled = false;
433
- currentAudio = null;
434
- };
435
-
436
- // Handle audio playback errors
437
- currentAudio.onerror = () => {
438
- console.error("Error playing audio.");
439
- alert("Could not play audio. Please try again.");
440
- buttonElement.classList.remove('loading');
441
- buttonElement.disabled = false;
442
- currentAudio = null;
443
- };
444
-
445
- } catch (error) {
446
- console.error("Error generating audio:", error);
447
- alert("Failed to generate audio. " + (error.message || "Please try again."));
448
- buttonElement.classList.remove('loading');
449
- buttonElement.disabled = false;
450
- }
451
- };
452
 
 
 
 
 
 
 
 
 
453
 
454
- // Fetches AI response from the backend
455
- const getAIResponse = async (userMessage) => {
456
- const indicator = showTypingIndicator(); // Show typing indicator
457
 
458
  try {
459
  const response = await fetch('/chat', {
460
  method: 'POST',
461
- headers: {
462
- 'Content-Type': 'application/json',
463
- },
464
  body: JSON.stringify({ message: userMessage })
465
  });
466
 
467
- const data = await response.json();
468
-
469
- if (data.error) {
470
- throw new Error(data.error);
471
  }
472
 
473
- // Remove typing indicator before adding the actual message
474
  chatArea.removeChild(indicator);
475
- // Add AI message with both HTML and plain text
476
- addMessage(data.response_html, 'ai', true, data.response_text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
  } catch (error) {
479
- chatArea.removeChild(indicator); // Remove indicator on error too
480
- addMessage("Sorry, I encountered an error. Please try again.", 'ai');
481
- console.error("Error:", error);
482
  }
483
- };
484
 
485
- // Event listener for form submission
486
  chatForm.addEventListener('submit', async (e) => {
487
- e.preventDefault(); // Prevent default form submission behavior (page reload)
488
  const message = messageInput.value.trim();
489
 
490
  if (message) {
491
- addMessage(message, 'user'); // Add user message to chat
492
- messageInput.value = ''; // Clear input field
493
- adjustTextareaHeight(); // Reset textarea height
494
 
495
- await getAIResponse(message); // Get AI response
496
  }
497
  });
498
 
499
- // Event listener for Enter key (send message) and Shift+Enter (new line)
500
  messageInput.addEventListener('keydown', (e) => {
501
  if (e.key === 'Enter' && !e.shiftKey) {
502
- e.preventDefault(); // Prevent new line
503
- chatForm.dispatchEvent(new Event('submit')); // Trigger form submission
504
  }
505
  });
506
 
507
- // Event listener for input changes to adjust textarea height and send button state
508
  messageInput.addEventListener('input', adjustTextareaHeight);
509
 
510
- // Attach button (placeholder functionality)
511
  attachButton.addEventListener('click', () => {
512
- // In a real app, this would open a file dialog
513
- alert("Attachment feature not implemented yet!");
514
- messageInput.focus(); // Keep focus on message input
515
  });
516
 
517
- // Initial setup on page load
518
- updateSendButtonState(); // Set initial state of send button
519
- messageInput.focus(); // Focus on the input field
520
 
521
- // Optional: Initial welcome message from AI on first visit
522
- const isFirstVisit = !localStorage.getItem('chatVisited');
523
- if (isFirstVisit) {
524
  localStorage.setItem('chatVisited', 'true');
525
- // Use a timeout to allow page to load fully before first message appears
526
  setTimeout(() => {
527
- // The welcome screen now has content, so we just let it display initially.
528
- // If you want an AI message to appear *after* the welcome screen and replace it,
529
- // you'd use addMessage here instead of directly rendering text in welcome-screen div.
530
- // For now, the welcome screen itself has the initial greeting.
531
- }, 500);
532
  }
533
  });
534
  </script>
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>AstroChat - AI Assistant</title>
7
  <style>
8
  :root {
9
  --background-color: #212121;
 
19
  --code-text-color: #f8f8f2;
20
  --link-color: #64b5f6;
21
  --quote-color: #4CAF50;
22
+ --audio-color: #00b4d8;
23
+ --error-color: #f44336;
24
  }
25
 
 
26
  * {
27
  box-sizing: border-box;
28
  margin: 0;
 
31
 
32
  html, body {
33
  height: 100%;
34
+ overflow: hidden;
35
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
36
  }
37
 
38
  body {
 
39
  background-color: var(--background-color);
40
  color: var(--text-color);
41
  display: flex;
42
  flex-direction: column;
 
43
  line-height: 1.6;
44
  }
45
 
46
  .chat-container {
47
  display: flex;
48
  flex-direction: column;
49
+ height: 100vh;
50
+ max-width: 800px;
51
+ margin: 0 auto;
52
  width: 100%;
 
 
 
 
53
  }
54
 
55
  .chat-area {
56
+ flex: 1;
57
+ overflow-y: auto;
58
  padding: 20px 16px;
59
+ scroll-behavior: smooth;
 
60
  }
61
 
 
62
  .welcome-screen {
63
  text-align: center;
64
+ margin: auto;
65
  color: var(--placeholder-color);
66
  padding: 20px;
67
+ animation: fadeIn 1s ease-out;
68
  }
69
 
70
  .welcome-screen h2 {
 
72
  font-weight: 400;
73
  margin-bottom: 10px;
74
  }
75
+
76
  .welcome-screen p {
77
  font-size: 1rem;
78
  line-height: 1.5;
79
+ margin-bottom: 8px;
80
  }
81
 
82
  .welcome-screen.hidden {
83
+ display: none;
84
  }
85
 
 
86
  .message {
87
+ max-width: 85%;
88
  padding: 12px 16px;
89
  border-radius: 18px;
90
  margin-bottom: 12px;
91
+ word-wrap: break-word;
92
  line-height: 1.6;
93
  font-size: 1rem;
94
+ position: relative;
95
+ animation: fadeIn 0.3s ease-out;
96
+ opacity: 0;
97
+ animation-fill-mode: forwards;
98
+ }
99
+
100
+ @keyframes fadeIn {
101
+ from { opacity: 0; transform: translateY(10px); }
102
+ to { opacity: 1; transform: translateY(0); }
103
  }
104
 
105
  .user-message {
106
  background-color: var(--user-bubble-color);
107
+ align-self: flex-end;
108
+ border-bottom-right-radius: 4px;
109
  }
110
 
111
  .ai-message {
112
  background-color: var(--ai-bubble-color);
113
+ align-self: flex-start;
114
+ border-bottom-left-radius: 4px;
115
+ padding-bottom: 30px;
116
  }
117
 
 
118
  .ai-message-content {
119
+ overflow: hidden;
120
+ }
121
+
122
+ .ai-message-content p {
123
+ margin-bottom: 12px;
124
+ }
125
+
126
+ .ai-message-content p:last-child {
127
+ margin-bottom: 0;
128
+ }
129
+
130
+ .ai-message-content strong {
131
+ color: #ffffff;
132
+ font-weight: 600;
133
+ }
134
+
135
+ .ai-message-content em {
136
+ color: #d1d1d1;
137
+ font-style: italic;
138
+ }
139
+
140
+ .ai-message-content a {
141
+ color: var(--link-color);
142
+ text-decoration: none;
143
+ word-break: break-all;
144
+ }
145
+
146
+ .ai-message-content a:hover {
147
+ text-decoration: underline;
148
+ }
149
+
150
+ .ai-message-content ul,
151
+ .ai-message-content ol {
152
+ padding-left: 24px;
153
+ margin-bottom: 12px;
154
+ }
155
+
156
+ .ai-message-content li {
157
+ margin-bottom: 6px;
158
+ }
159
+
160
  .ai-message-content blockquote {
161
  border-left: 3px solid var(--quote-color);
162
  padding-left: 12px;
 
164
  color: #bdbdbd;
165
  margin-bottom: 12px;
166
  }
167
+
168
  .ai-message-content pre {
169
  background-color: var(--code-bg-color);
170
  border-radius: 6px;
171
  padding: 12px;
172
+ overflow-x: auto;
173
  margin-bottom: 12px;
174
  }
175
+
176
  .ai-message-content code {
177
  font-family: 'Courier New', Courier, monospace;
178
  background-color: var(--code-bg-color);
 
180
  border-radius: 3px;
181
  color: var(--code-text-color);
182
  font-size: 0.9em;
183
+ white-space: pre-wrap;
184
  }
185
+
186
  .ai-message-content .code-block code {
187
+ padding: 0;
188
+ background-color: transparent;
189
+ color: inherit;
190
  }
191
 
 
192
  .audio-button {
193
  position: absolute;
194
  bottom: 6px;
195
  right: 8px;
196
+ background: rgba(0, 180, 216, 0.1);
197
  border: none;
198
  border-radius: 50%;
199
  width: 30px;
 
202
  align-items: center;
203
  justify-content: center;
204
  cursor: pointer;
205
+ transition: all 0.2s;
206
+ z-index: 10;
207
  }
208
+
209
  .audio-button:hover {
210
+ background: rgba(0, 180, 216, 0.2);
211
+ transform: scale(1.1);
212
  }
213
+
214
+ .audio-button.playing {
215
+ background: rgba(0, 180, 216, 0.3);
216
+ box-shadow: 0 0 0 2px var(--audio-color);
 
217
  }
218
+
219
+ .audio-button.playing svg {
220
+ fill: var(--audio-color);
221
  }
222
+
223
  .audio-button.loading .speaker-icon {
224
+ display: none;
225
  }
226
+
227
  .audio-button .loader {
228
+ display: none;
229
  width: 18px;
230
  height: 18px;
231
+ border: 2px solid #f3f3f3;
232
+ border-top: 2px solid var(--audio-color);
233
  border-radius: 50%;
234
+ animation: spin 1s linear infinite;
235
  }
236
+
237
  .audio-button.loading .loader {
238
+ display: block;
239
  }
240
+
241
  @keyframes spin {
242
  0% { transform: rotate(0deg); }
243
  100% { transform: rotate(360deg); }
244
  }
245
 
 
246
  .typing-indicator {
247
  display: flex;
248
  align-items: center;
249
  padding: 12px 16px;
250
  }
251
+
252
  .typing-indicator span {
253
+ height: 8px;
254
+ width: 8px;
255
+ background-color: var(--icon-color);
256
+ border-radius: 50%;
257
+ display: inline-block;
258
+ margin: 0 2px;
259
  animation: bounce 1.4s infinite both;
260
  }
261
+
262
+ .typing-indicator span:nth-child(2) {
263
+ animation-delay: 0.2s;
264
+ }
265
+
266
+ .typing-indicator span:nth-child(3) {
267
+ animation-delay: 0.4s;
268
+ }
269
 
270
  @keyframes bounce {
271
  0%, 80%, 100% { transform: scale(0); }
272
  40% { transform: scale(1.0); }
273
  }
274
 
 
275
  .input-area {
276
  display: flex;
277
+ align-items: flex-end;
278
  padding: 10px 16px;
279
  border-top: 1px solid var(--border-color);
280
  background-color: var(--input-area-color);
281
+ flex-shrink: 0;
282
  }
283
+
284
  .input-wrapper {
285
  display: flex;
286
  align-items: center;
287
  width: 100%;
288
  background-color: var(--user-bubble-color);
289
+ border-radius: 24px;
290
  padding: 4px;
291
  }
292
+
293
  .input-area textarea {
294
+ flex-grow: 1;
295
  border: none;
296
  background: transparent;
297
  color: var(--text-color);
298
+ resize: none;
299
  font-size: 1rem;
300
  line-height: 1.5;
301
+ max-height: 120px;
302
+ overflow-y: auto;
303
  padding: 8px 12px;
304
+ font-family: inherit;
305
+ outline: none;
306
+ }
307
+
308
+ .input-area textarea::placeholder {
309
+ color: var(--placeholder-color);
310
  }
 
311
 
312
+ .icon-button {
 
313
  background: none;
314
  border: none;
315
  padding: 8px;
 
319
  justify-content: center;
320
  transition: opacity 0.2s;
321
  }
322
+
323
+ .icon-button:hover {
324
  opacity: 0.8;
325
  }
326
+
327
+ .icon-button svg {
328
+ width: 24px;
329
+ height: 24px;
330
+ fill: var(--icon-color);
331
+ }
332
+
333
  .send-button {
334
  background-color: var(--send-button-color);
335
  border-radius: 50%;
336
  padding: 8px;
337
  transition: background-color 0.2s, opacity 0.2s;
338
  }
339
+
340
  .send-button.disabled {
341
  background-color: #555;
342
  cursor: not-allowed;
343
  opacity: 0.6;
344
  }
345
+
346
  .send-button.disabled svg {
347
  fill: #888;
348
  }
 
349
 
350
+ .send-button svg {
351
+ fill: white;
352
+ width: 24px;
353
+ height: 24px;
354
+ }
355
+
356
+ .error-message {
357
+ color: var(--error-color);
358
+ padding: 8px 16px;
359
+ text-align: center;
360
+ font-size: 0.9rem;
361
+ animation: fadeIn 0.3s ease-out;
362
  }
363
  </style>
364
  </head>
365
  <body>
366
  <div class="chat-container">
367
+ <main class="chat-area" id="chat-area">
368
  <div class="welcome-screen">
369
  <h2>Welcome to AstroChat!</h2>
370
+ <p>Hello! I'm your AI assistant powered by Gemini. I can help with explanations, ideas, and even read responses aloud.</p>
371
+ <p>Try saying: <em>"Explain quantum computing"</em> or <em>"Read this poem aloud"</em></p>
372
  </div>
373
  </main>
374
 
375
  <footer class="input-area">
376
  <form id="chat-form" class="input-wrapper">
377
+ <button type="button" class="icon-button" id="attach-button" aria-label="Attach file">
378
  <svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .83-.67 1.5-1.5 1.5s-1.5-.67-1.5-1.5V6H10v9.5a2.5 2.5 0 0 0 5 0V5c-1.38 0-2.5 1.12-2.5 2.5v10.5c0 1.38-1.12 2.5-2.5 2.5s-2.5-1.12-2.5-2.5V5a4 4 0 0 1 8 0v11.5c-1.1 0-2-.9-2-2V6h-1.5z"></path></svg>
379
  </button>
380
+ <textarea id="message-input" placeholder="Ask anything..." rows="1" aria-label="Message input"></textarea>
381
+ <button type="submit" id="send-button" class="icon-button send-button disabled" aria-label="Send message">
382
  <svg viewBox="0 0 24 24"><path d="M2 21l21-9L2 3v7l15 2-15 2v7z"></path></svg>
383
  </button>
384
  </form>
 
389
  document.addEventListener('DOMContentLoaded', () => {
390
  const chatForm = document.getElementById('chat-form');
391
  const messageInput = document.getElementById('message-input');
392
+ const chatArea = document.getElementById('chat-area');
393
  const welcomeScreen = document.querySelector('.welcome-screen');
394
  const sendButton = document.getElementById('send-button');
395
  const attachButton = document.getElementById('attach-button');
396
 
397
+ // Audio state management
398
+ const audioPlayer = {
399
+ currentAudio: null,
400
+ currentButton: null,
401
+ isPlaying: false,
402
+
403
+ play: async function(buttonElement, text) {
404
+ // Stop any current playback
405
+ this.stop();
406
+
407
+ // Set loading state
408
+ buttonElement.classList.add('loading');
409
+ buttonElement.disabled = true;
410
+
411
+ try {
412
+ // Generate audio
413
+ const response = await fetch('/generate-audio', {
414
+ method: 'POST',
415
+ headers: { 'Content-Type': 'application/json' },
416
+ body: JSON.stringify({ text })
417
+ });
418
+
419
+ if (!response.ok) {
420
+ throw new Error(await response.text());
421
+ }
422
+
423
+ const data = await response.json();
424
+
425
+ // Create new audio element
426
+ this.currentAudio = new Audio(data.audio_url);
427
+ this.currentButton = buttonElement;
428
+
429
+ // Event handlers
430
+ this.currentAudio.onplay = () => {
431
+ this.isPlaying = true;
432
+ buttonElement.classList.remove('loading');
433
+ buttonElement.classList.add('playing');
434
+ };
435
+
436
+ this.currentAudio.onended = () => {
437
+ this.reset();
438
+ };
439
+
440
+ this.currentAudio.onerror = () => {
441
+ this.reset();
442
+ showError("Audio playback failed");
443
+ };
444
+
445
+ // Start playback
446
+ this.currentAudio.play();
447
+
448
+ } catch (error) {
449
+ console.error("Audio error:", error);
450
+ buttonElement.classList.remove('loading', 'playing');
451
+ showError("Couldn't generate audio");
452
+ }
453
+ },
454
+
455
+ stop: function() {
456
+ if (this.currentAudio) {
457
+ this.currentAudio.pause();
458
+ this.currentAudio.currentTime = 0;
459
+ }
460
+ if (this.currentButton) {
461
+ this.currentButton.classList.remove('playing');
462
+ }
463
+ this.reset();
464
+ },
465
+
466
+ reset: function() {
467
+ this.isPlaying = false;
468
+ this.currentAudio = null;
469
+ if (this.currentButton) {
470
+ this.currentButton.classList.remove('loading', 'playing');
471
+ this.currentButton.disabled = false;
472
+ }
473
+ this.currentButton = null;
474
+ }
475
  };
476
 
477
+ // UI Helpers
478
+ function adjustTextareaHeight() {
479
+ messageInput.style.height = 'auto';
480
+ messageInput.style.height = `${messageInput.scrollHeight}px`;
481
+ updateSendButtonState();
482
+ }
483
+
484
+ function updateSendButtonState() {
485
  const isDisabled = messageInput.value.trim() === '';
486
  sendButton.classList.toggle('disabled', isDisabled);
487
  sendButton.disabled = isDisabled;
488
+ }
489
+
490
+ function scrollToBottom() {
 
491
  chatArea.scrollTop = chatArea.scrollHeight;
492
+ }
493
+
494
+ function showError(message) {
495
+ const existingError = document.querySelector('.error-message');
496
+ if (existingError) {
497
+ existingError.remove();
498
+ }
499
+
500
+ const errorElement = document.createElement('div');
501
+ errorElement.className = 'error-message';
502
+ errorElement.textContent = message;
503
+ chatArea.appendChild(errorElement);
504
+ scrollToBottom();
505
+
506
+ setTimeout(() => {
507
+ errorElement.remove();
508
+ }, 5000);
509
+ }
510
 
511
+ // Message handling
512
+ function addMessage(content, sender, isHTML = false, plainText = '') {
513
+ welcomeScreen.classList.add('hidden');
 
514
 
515
  const messageElement = document.createElement('div');
516
+ messageElement.className = `message ${sender}-message`;
517
 
518
  const contentDiv = document.createElement('div');
519
  if (isHTML) {
520
+ contentDiv.className = `${sender}-message-content`;
521
+ contentDiv.innerHTML = content;
522
  } else {
523
+ contentDiv.textContent = content;
524
  }
525
+
526
  messageElement.appendChild(contentDiv);
527
+
528
  // Add audio button for AI messages
529
  if (sender === 'ai' && plainText) {
530
  const audioButton = document.createElement('button');
531
+ audioButton.className = 'audio-button';
532
+ audioButton.title = "Play audio";
 
533
  audioButton.innerHTML = `
534
  <svg class="speaker-icon" viewBox="0 0 24 24">
535
  <path d="M3 10v4c0 .55.45 1 1 1h3.5l5.5 5V5L7.5 9H4c-.55 0-1 .45-1 1zm14 0h-1.5c-2.31 0-4.22-1.74-4.47-4H10v12h1.5v-2.22c.25-2.26 2.16-4.22 4.47-4.22H17c1.1 0 2 .9 2 2s-.9 2-2 2h-1.5v2H17c2.21 0 4-1.79 4-4s-1.79-4-4-4z"/>
536
  </svg>
537
  <div class="loader"></div>
538
  `;
539
+
540
+ audioButton.addEventListener('click', () => {
541
+ audioPlayer.play(audioButton, plainText);
542
+ });
543
+
544
  messageElement.appendChild(audioButton);
545
  }
546
 
547
  chatArea.appendChild(messageElement);
548
  scrollToBottom();
549
  return messageElement;
550
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
+ function showTypingIndicator() {
553
+ const indicator = document.createElement('div');
554
+ indicator.className = 'message ai-message typing-indicator';
555
+ indicator.innerHTML = '<span></span><span></span><span></span>';
556
+ chatArea.appendChild(indicator);
557
+ scrollToBottom();
558
+ return indicator;
559
+ }
560
 
561
+ // AI Communication
562
+ async function getAIResponse(userMessage) {
563
+ const indicator = showTypingIndicator();
564
 
565
  try {
566
  const response = await fetch('/chat', {
567
  method: 'POST',
568
+ headers: { 'Content-Type': 'application/json' },
 
 
569
  body: JSON.stringify({ message: userMessage })
570
  });
571
 
572
+ if (!response.ok) {
573
+ throw new Error(await response.text());
 
 
574
  }
575
 
576
+ const data = await response.json();
577
  chatArea.removeChild(indicator);
578
+
579
+ const messageElement = addMessage(
580
+ data.response_html,
581
+ 'ai',
582
+ true,
583
+ data.response_text
584
+ );
585
+
586
+ // Auto-play if audio was requested
587
+ if (data.audio_requested) {
588
+ const audioButton = messageElement.querySelector('.audio-button');
589
+ if (audioButton) {
590
+ setTimeout(() => {
591
+ audioPlayer.play(audioButton, data.response_text);
592
+ }, 300);
593
+ }
594
+ }
595
 
596
  } catch (error) {
597
+ console.error("Chat error:", error);
598
+ chatArea.removeChild(indicator);
599
+ showError("Failed to get response from AI");
600
  }
601
+ }
602
 
603
+ // Event Listeners
604
  chatForm.addEventListener('submit', async (e) => {
605
+ e.preventDefault();
606
  const message = messageInput.value.trim();
607
 
608
  if (message) {
609
+ addMessage(message, 'user');
610
+ messageInput.value = '';
611
+ adjustTextareaHeight();
612
 
613
+ await getAIResponse(message);
614
  }
615
  });
616
 
 
617
  messageInput.addEventListener('keydown', (e) => {
618
  if (e.key === 'Enter' && !e.shiftKey) {
619
+ e.preventDefault();
620
+ chatForm.dispatchEvent(new Event('submit'));
621
  }
622
  });
623
 
 
624
  messageInput.addEventListener('input', adjustTextareaHeight);
625
 
 
626
  attachButton.addEventListener('click', () => {
627
+ // Future implementation for file attachments
628
+ showError("File attachments coming soon!");
629
+ messageInput.focus();
630
  });
631
 
632
+ // Initial setup
633
+ updateSendButtonState();
634
+ messageInput.focus();
635
 
636
+ // First visit detection
637
+ if (!localStorage.getItem('chatVisited')) {
 
638
  localStorage.setItem('chatVisited', 'true');
 
639
  setTimeout(() => {
640
+ addMessage(
641
+ "Pro tip: You can say 'read aloud' to hear my responses!",
642
+ 'ai'
643
+ );
644
+ }, 3000);
645
  }
646
  });
647
  </script>