jeremierostan commited on
Commit
87ab4e6
·
verified ·
1 Parent(s): 69e88b4

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +41 -144
index.html CHANGED
@@ -1,6 +1,5 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
-
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -70,9 +69,7 @@
70
  font-size: 0.875rem;
71
  font-weight: 500;
72
  }
73
- input,
74
- select,
75
- textarea {
76
  padding: 0.75rem;
77
  border-radius: 0.5rem;
78
  border: 1px solid rgba(255, 255, 255, 0.1);
@@ -99,45 +96,6 @@
99
  opacity: 0.9;
100
  transform: translateY(-1px);
101
  }
102
- .icon-with-spinner {
103
- display: flex;
104
- align-items: center;
105
- justify-content: center;
106
- gap: 12px;
107
- min-width: 180px;
108
- }
109
- .spinner {
110
- width: 20px;
111
- height: 20px;
112
- border: 2px solid white;
113
- border-top-color: transparent;
114
- border-radius: 50%;
115
- animation: spin 1s linear infinite;
116
- flex-shrink: 0;
117
- }
118
- @keyframes spin {
119
- to {
120
- transform: rotate(360deg);
121
- }
122
- }
123
- .pulse-container {
124
- display: flex;
125
- align-items: center;
126
- justify-content: center;
127
- gap: 12px;
128
- min-width: 180px;
129
- }
130
- .pulse-circle {
131
- width: 20px;
132
- height: 20px;
133
- border-radius: 50%;
134
- background-color: white;
135
- opacity: 0.2;
136
- flex-shrink: 0;
137
- transform: translateX(-0%) scale(var(--audio-level, 1));
138
- transition: transform 0.1s ease;
139
- }
140
- /* Add styles for toast notifications */
141
  .toast {
142
  position: fixed;
143
  top: 20px;
@@ -158,12 +116,6 @@
158
  background-color: #ffd700;
159
  color: black;
160
  }
161
- .prompt-selection {
162
- border: 1px solid rgba(255, 255, 255, 0.1);
163
- border-radius: 0.5rem;
164
- padding: 1rem;
165
- background-color: rgba(0, 0, 0, 0.1);
166
- }
167
  .radio-group {
168
  display: flex;
169
  flex-direction: column;
@@ -176,12 +128,6 @@
176
  gap: 0.5rem;
177
  cursor: pointer;
178
  }
179
- .radio-option input[type="radio"] {
180
- margin: 0;
181
- width: 16px;
182
- height: 16px;
183
- cursor: pointer;
184
- }
185
  .prompt-preview {
186
  padding: 0.5rem;
187
  font-size: 0.8rem;
@@ -197,24 +143,16 @@
197
  text-decoration: underline;
198
  cursor: pointer;
199
  font-size: 0.8rem;
200
- margin-top: 0.25rem;
201
  margin-left: 0.5rem;
202
- display: inline-block;
203
  }
204
  </style>
205
  </head>
206
-
207
-
208
  <body>
209
- <!-- Add toast element after body opening tag -->
210
  <div id="error-toast" class="toast"></div>
211
  <div style="text-align: center">
212
  <h1>Gemini Voice Chat</h1>
213
  <p>Speak with Gemini using real-time audio streaming</p>
214
- <p>
215
- Get a Gemini API key
216
- <a href="https://ai.google.dev/gemini-api/docs/api-key">here</a>
217
- </p>
218
  </div>
219
  <div class="container">
220
  <div class="controls">
@@ -235,7 +173,7 @@
235
 
236
  <div class="input-group">
237
  <label>System Prompt</label>
238
- <div class="prompt-selection">
239
  <div class="radio-group" id="prompt-options">
240
  <label class="radio-option">
241
  <input type="radio" name="prompt-selection" value="default" checked>
@@ -285,10 +223,9 @@
285
  <audio id="audio-output"></audio>
286
 
287
  <script>
288
- // System prompts data injected from the server
289
  const SYSTEM_PROMPTS = __SYSTEM_PROMPTS__;
290
 
291
- // Initialize prompt previews
292
  function initPromptPreviews() {
293
  const previews = document.querySelectorAll('.prompt-preview');
294
  previews.forEach(preview => {
@@ -299,7 +236,6 @@
299
  });
300
  }
301
 
302
- // Toggle prompt preview visibility
303
  function togglePromptPreview(element) {
304
  const preview = element.nextElementSibling;
305
  if (preview.style.display === 'block') {
@@ -307,11 +243,9 @@
307
  } else {
308
  preview.style.display = 'block';
309
  }
310
- // Prevent the radio button from being toggled
311
  event.stopPropagation();
312
  }
313
 
314
- // Show/hide custom prompt textarea based on selection
315
  function handlePromptSelection() {
316
  const customPromptContainer = document.getElementById('custom-prompt-container');
317
  const selectedValue = document.querySelector('input[name="prompt-selection"]:checked').value;
@@ -323,7 +257,6 @@
323
  }
324
  }
325
 
326
- // Initialize event listeners for radio buttons
327
  function initRadioListeners() {
328
  const radioButtons = document.querySelectorAll('input[name="prompt-selection"]');
329
  radioButtons.forEach(radio => {
@@ -331,7 +264,6 @@
331
  });
332
  }
333
 
334
- // Initialize on page load
335
  document.addEventListener('DOMContentLoaded', function() {
336
  initPromptPreviews();
337
  initRadioListeners();
@@ -342,48 +274,41 @@
342
  let dataChannel;
343
  let isRecording = false;
344
  let webrtc_id;
 
 
345
  const startButton = document.getElementById('start-button');
346
  const apiKeyInput = document.getElementById('api-key');
347
  const voiceSelect = document.getElementById('voice');
348
  const audioOutput = document.getElementById('audio-output');
349
  const boxContainer = document.querySelector('.box-container');
 
350
  const numBars = 32;
351
  for (let i = 0; i < numBars; i++) {
352
  const box = document.createElement('div');
353
  box.className = 'box';
354
  boxContainer.appendChild(box);
355
  }
 
356
  function updateButtonState() {
357
  if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
358
- startButton.innerHTML = `
359
- <div class="icon-with-spinner">
360
- <div class="spinner"></div>
361
- <span>Connecting...</span>
362
- </div>
363
- `;
364
  } else if (peerConnection && peerConnection.connectionState === 'connected') {
365
- startButton.innerHTML = `
366
- <div class="pulse-container">
367
- <div class="pulse-circle"></div>
368
- <span>Stop Recording</span>
369
- </div>
370
- `;
371
  } else {
372
  startButton.innerHTML = 'Start Recording';
373
  }
374
  }
 
375
  function showError(message) {
376
  const toast = document.getElementById('error-toast');
377
  toast.textContent = message;
378
  toast.className = 'toast error';
379
  toast.style.display = 'block';
380
- // Hide toast after 5 seconds
381
  setTimeout(() => {
382
  toast.style.display = 'none';
383
  }, 5000);
384
  }
385
 
386
- // Get the currently selected prompt
387
  function getSelectedPrompt() {
388
  const selectedValue = document.querySelector('input[name="prompt-selection"]:checked').value;
389
  const customPrompt = document.getElementById('custom-prompt').value;
@@ -398,26 +323,18 @@
398
  const config = __RTC_CONFIGURATION__;
399
  peerConnection = new RTCPeerConnection(config);
400
  webrtc_id = Math.random().toString(36).substring(7);
401
- const timeoutId = setTimeout(() => {
402
- const toast = document.getElementById('error-toast');
403
- toast.textContent = "Connection is taking longer than usual. Are you on a VPN?";
404
- toast.className = 'toast warning';
405
- toast.style.display = 'block';
406
- // Hide warning after 5 seconds
407
- setTimeout(() => {
408
- toast.style.display = 'none';
409
- }, 5000);
410
- }, 5000);
411
  try {
412
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
413
  stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
414
- // Update audio visualization setup
415
  audioContext = new AudioContext();
416
- analyser_input = audioContext.createAnalyser();
417
  const source = audioContext.createMediaStreamSource(stream);
418
  source.connect(analyser_input);
419
  analyser_input.fftSize = 64;
420
- dataArray_input = new Uint8Array(analyser_input.frequencyBinCount);
 
421
  function updateAudioLevel() {
422
  analyser_input.getByteFrequencyData(dataArray_input);
423
  const average = Array.from(dataArray_input).reduce((a, b) => a + b, 0) / dataArray_input.length;
@@ -429,32 +346,36 @@
429
  animationId = requestAnimationFrame(updateAudioLevel);
430
  }
431
  updateAudioLevel();
432
- // Add connection state change listener
433
  peerConnection.addEventListener('connectionstatechange', () => {
434
- console.log('connectionstatechange', peerConnection.connectionState);
435
- if (peerConnection.connectionState === 'connected') {
436
- clearTimeout(timeoutId);
437
- const toast = document.getElementById('error-toast');
438
- toast.style.display = 'none';
439
- }
440
  updateButtonState();
441
  });
442
- // Handle incoming audio
443
  peerConnection.addEventListener('track', (evt) => {
444
  if (audioOutput && audioOutput.srcObject !== evt.streams[0]) {
445
  audioOutput.srcObject = evt.streams[0];
446
  audioOutput.play();
447
- // Set up audio visualization on the output stream
448
  audioContext = new AudioContext();
449
- analyser = audioContext.createAnalyser();
450
  const source = audioContext.createMediaStreamSource(evt.streams[0]);
451
  source.connect(analyser);
452
  analyser.fftSize = 2048;
453
- dataArray = new Uint8Array(analyser.frequencyBinCount);
 
 
 
 
 
 
 
 
 
 
454
  updateVisualization();
455
  }
456
  });
457
- // Create data channel for messages
458
  dataChannel = peerConnection.createDataChannel('text');
459
  dataChannel.onmessage = (event) => {
460
  const eventJson = JSON.parse(event.data);
@@ -478,22 +399,10 @@
478
  });
479
  }
480
  };
481
- // Create and send offer
482
  const offer = await peerConnection.createOffer();
483
  await peerConnection.setLocalDescription(offer);
484
- await new Promise((resolve) => {
485
- if (peerConnection.iceGatheringState === "complete") {
486
- resolve();
487
- } else {
488
- const checkState = () => {
489
- if (peerConnection.iceGatheringState === "complete") {
490
- peerConnection.removeEventListener("icegatheringstatechange", checkState);
491
- resolve();
492
- }
493
- };
494
- peerConnection.addEventListener("icegatheringstatechange", checkState);
495
- }
496
- });
497
  const response = await fetch('/webrtc/offer', {
498
  method: 'POST',
499
  headers: { 'Content-Type': 'application/json' },
@@ -503,34 +412,22 @@
503
  webrtc_id: webrtc_id,
504
  })
505
  });
 
506
  const serverResponse = await response.json();
507
  if (serverResponse.status === 'failed') {
508
- showError(serverResponse.meta.error === 'concurrency_limit_reached'
509
- ? `Too many connections. Maximum limit is ${serverResponse.meta.limit}`
510
- : serverResponse.meta.error);
511
- stop();
512
- startButton.textContent = 'Start Recording';
513
  return;
514
  }
 
515
  await peerConnection.setRemoteDescription(serverResponse);
516
  } catch (err) {
517
- clearTimeout(timeoutId);
518
  console.error('Error setting up WebRTC:', err);
519
  showError('Failed to establish connection. Please try again.');
520
- stop();
521
- startButton.textContent = 'Start Recording';
522
- }
523
- }
524
- function updateVisualization() {
525
- if (!analyser) return;
526
- analyser.getByteFrequencyData(dataArray);
527
- const bars = document.querySelectorAll('.box');
528
- for (let i = 0; i < bars.length; i++) {
529
- const barHeight = (dataArray[i] / 255) * 2;
530
- bars[i].style.transform = `scaleY(${Math.max(0.1, barHeight)})`;
531
  }
532
- animationId = requestAnimationFrame(updateVisualization);
533
  }
 
534
  function stopWebRTC() {
535
  if (peerConnection) {
536
  peerConnection.close();
@@ -543,6 +440,7 @@
543
  }
544
  updateButtonState();
545
  }
 
546
  startButton.addEventListener('click', () => {
547
  if (!isRecording) {
548
  setupWebRTC();
@@ -555,5 +453,4 @@
555
  });
556
  </script>
557
  </body>
558
-
559
  </html>
 
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">
 
69
  font-size: 0.875rem;
70
  font-weight: 500;
71
  }
72
+ input, select, textarea {
 
 
73
  padding: 0.75rem;
74
  border-radius: 0.5rem;
75
  border: 1px solid rgba(255, 255, 255, 0.1);
 
96
  opacity: 0.9;
97
  transform: translateY(-1px);
98
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  .toast {
100
  position: fixed;
101
  top: 20px;
 
116
  background-color: #ffd700;
117
  color: black;
118
  }
 
 
 
 
 
 
119
  .radio-group {
120
  display: flex;
121
  flex-direction: column;
 
128
  gap: 0.5rem;
129
  cursor: pointer;
130
  }
 
 
 
 
 
 
131
  .prompt-preview {
132
  padding: 0.5rem;
133
  font-size: 0.8rem;
 
143
  text-decoration: underline;
144
  cursor: pointer;
145
  font-size: 0.8rem;
 
146
  margin-left: 0.5rem;
 
147
  }
148
  </style>
149
  </head>
 
 
150
  <body>
 
151
  <div id="error-toast" class="toast"></div>
152
  <div style="text-align: center">
153
  <h1>Gemini Voice Chat</h1>
154
  <p>Speak with Gemini using real-time audio streaming</p>
155
+ <p>Get a Gemini API key <a href="https://ai.google.dev/gemini-api/docs/api-key">here</a></p>
 
 
 
156
  </div>
157
  <div class="container">
158
  <div class="controls">
 
173
 
174
  <div class="input-group">
175
  <label>System Prompt</label>
176
+ <div style="border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 0.5rem; padding: 1rem; background-color: rgba(0, 0, 0, 0.1);">
177
  <div class="radio-group" id="prompt-options">
178
  <label class="radio-option">
179
  <input type="radio" name="prompt-selection" value="default" checked>
 
223
  <audio id="audio-output"></audio>
224
 
225
  <script>
226
+ // System prompts data
227
  const SYSTEM_PROMPTS = __SYSTEM_PROMPTS__;
228
 
 
229
  function initPromptPreviews() {
230
  const previews = document.querySelectorAll('.prompt-preview');
231
  previews.forEach(preview => {
 
236
  });
237
  }
238
 
 
239
  function togglePromptPreview(element) {
240
  const preview = element.nextElementSibling;
241
  if (preview.style.display === 'block') {
 
243
  } else {
244
  preview.style.display = 'block';
245
  }
 
246
  event.stopPropagation();
247
  }
248
 
 
249
  function handlePromptSelection() {
250
  const customPromptContainer = document.getElementById('custom-prompt-container');
251
  const selectedValue = document.querySelector('input[name="prompt-selection"]:checked').value;
 
257
  }
258
  }
259
 
 
260
  function initRadioListeners() {
261
  const radioButtons = document.querySelectorAll('input[name="prompt-selection"]');
262
  radioButtons.forEach(radio => {
 
264
  });
265
  }
266
 
 
267
  document.addEventListener('DOMContentLoaded', function() {
268
  initPromptPreviews();
269
  initRadioListeners();
 
274
  let dataChannel;
275
  let isRecording = false;
276
  let webrtc_id;
277
+ let animationId;
278
+
279
  const startButton = document.getElementById('start-button');
280
  const apiKeyInput = document.getElementById('api-key');
281
  const voiceSelect = document.getElementById('voice');
282
  const audioOutput = document.getElementById('audio-output');
283
  const boxContainer = document.querySelector('.box-container');
284
+
285
  const numBars = 32;
286
  for (let i = 0; i < numBars; i++) {
287
  const box = document.createElement('div');
288
  box.className = 'box';
289
  boxContainer.appendChild(box);
290
  }
291
+
292
  function updateButtonState() {
293
  if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
294
+ startButton.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;gap:12px;min-width:180px"><div style="width:20px;height:20px;border:2px solid white;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite;flex-shrink:0"></div><span>Connecting...</span></div>';
 
 
 
 
 
295
  } else if (peerConnection && peerConnection.connectionState === 'connected') {
296
+ startButton.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;gap:12px;min-width:180px"><div class="pulse-circle" style="width:20px;height:20px;border-radius:50%;background-color:white;opacity:0.2;flex-shrink:0"></div><span>Stop Recording</span></div>';
 
 
 
 
 
297
  } else {
298
  startButton.innerHTML = 'Start Recording';
299
  }
300
  }
301
+
302
  function showError(message) {
303
  const toast = document.getElementById('error-toast');
304
  toast.textContent = message;
305
  toast.className = 'toast error';
306
  toast.style.display = 'block';
 
307
  setTimeout(() => {
308
  toast.style.display = 'none';
309
  }, 5000);
310
  }
311
 
 
312
  function getSelectedPrompt() {
313
  const selectedValue = document.querySelector('input[name="prompt-selection"]:checked').value;
314
  const customPrompt = document.getElementById('custom-prompt').value;
 
323
  const config = __RTC_CONFIGURATION__;
324
  peerConnection = new RTCPeerConnection(config);
325
  webrtc_id = Math.random().toString(36).substring(7);
326
+
 
 
 
 
 
 
 
 
 
327
  try {
328
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
329
  stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
330
+
331
  audioContext = new AudioContext();
332
+ const analyser_input = audioContext.createAnalyser();
333
  const source = audioContext.createMediaStreamSource(stream);
334
  source.connect(analyser_input);
335
  analyser_input.fftSize = 64;
336
+ const dataArray_input = new Uint8Array(analyser_input.frequencyBinCount);
337
+
338
  function updateAudioLevel() {
339
  analyser_input.getByteFrequencyData(dataArray_input);
340
  const average = Array.from(dataArray_input).reduce((a, b) => a + b, 0) / dataArray_input.length;
 
346
  animationId = requestAnimationFrame(updateAudioLevel);
347
  }
348
  updateAudioLevel();
349
+
350
  peerConnection.addEventListener('connectionstatechange', () => {
 
 
 
 
 
 
351
  updateButtonState();
352
  });
353
+
354
  peerConnection.addEventListener('track', (evt) => {
355
  if (audioOutput && audioOutput.srcObject !== evt.streams[0]) {
356
  audioOutput.srcObject = evt.streams[0];
357
  audioOutput.play();
358
+
359
  audioContext = new AudioContext();
360
+ const analyser = audioContext.createAnalyser();
361
  const source = audioContext.createMediaStreamSource(evt.streams[0]);
362
  source.connect(analyser);
363
  analyser.fftSize = 2048;
364
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
365
+
366
+ function updateVisualization() {
367
+ analyser.getByteFrequencyData(dataArray);
368
+ const bars = document.querySelectorAll('.box');
369
+ for (let i = 0; i < bars.length; i++) {
370
+ const barHeight = (dataArray[i] / 255) * 2;
371
+ bars[i].style.transform = `scaleY(${Math.max(0.1, barHeight)})`;
372
+ }
373
+ animationId = requestAnimationFrame(updateVisualization);
374
+ }
375
  updateVisualization();
376
  }
377
  });
378
+
379
  dataChannel = peerConnection.createDataChannel('text');
380
  dataChannel.onmessage = (event) => {
381
  const eventJson = JSON.parse(event.data);
 
399
  });
400
  }
401
  };
402
+
403
  const offer = await peerConnection.createOffer();
404
  await peerConnection.setLocalDescription(offer);
405
+
 
 
 
 
 
 
 
 
 
 
 
 
406
  const response = await fetch('/webrtc/offer', {
407
  method: 'POST',
408
  headers: { 'Content-Type': 'application/json' },
 
412
  webrtc_id: webrtc_id,
413
  })
414
  });
415
+
416
  const serverResponse = await response.json();
417
  if (serverResponse.status === 'failed') {
418
+ showError(serverResponse.meta.error);
419
+ stopWebRTC();
 
 
 
420
  return;
421
  }
422
+
423
  await peerConnection.setRemoteDescription(serverResponse);
424
  } catch (err) {
 
425
  console.error('Error setting up WebRTC:', err);
426
  showError('Failed to establish connection. Please try again.');
427
+ stopWebRTC();
 
 
 
 
 
 
 
 
 
 
428
  }
 
429
  }
430
+
431
  function stopWebRTC() {
432
  if (peerConnection) {
433
  peerConnection.close();
 
440
  }
441
  updateButtonState();
442
  }
443
+
444
  startButton.addEventListener('click', () => {
445
  if (!isRecording) {
446
  setupWebRTC();
 
453
  });
454
  </script>
455
  </body>
 
456
  </html>