jeremierostan commited on
Commit
4cd60b2
·
verified ·
1 Parent(s): 05a24ef

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +165 -123
index.html CHANGED
@@ -1,5 +1,6 @@
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,7 +70,9 @@
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,6 +99,45 @@
96
  opacity: 0.9;
97
  transform: translateY(-1px);
98
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  .toast {
100
  position: fixed;
101
  top: 20px;
@@ -116,17 +158,13 @@
116
  background-color: #ffd700;
117
  color: black;
118
  }
119
- .radio-group {
120
- display: flex;
121
- flex-direction: column;
122
- gap: 0.5rem;
123
- margin-top: 0.5rem;
124
- }
125
- .radio-option {
126
- display: flex;
127
- align-items: center;
128
- gap: 0.5rem;
129
- cursor: pointer;
130
  }
131
  .prompt-preview {
132
  padding: 0.5rem;
@@ -134,7 +172,7 @@
134
  margin-top: 0.25rem;
135
  background-color: rgba(0, 0, 0, 0.2);
136
  border-radius: 0.25rem;
137
- max-height: 100px;
138
  overflow-y: auto;
139
  display: none;
140
  }
@@ -147,12 +185,17 @@
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,40 +216,16 @@
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>
180
- Default
181
- <span class="prompt-toggle" onclick="togglePromptPreview(this)">Show/Hide</span>
182
- <div class="prompt-preview"></div>
183
- </label>
184
- <label class="radio-option">
185
- <input type="radio" name="prompt-selection" value="Behavior Analyst">
186
- Behavior Analyst
187
- <span class="prompt-toggle" onclick="togglePromptPreview(this)">Show/Hide</span>
188
- <div class="prompt-preview"></div>
189
- </label>
190
- <label class="radio-option">
191
- <input type="radio" name="prompt-selection" value="UDL Expert">
192
- UDL Expert
193
- <span class="prompt-toggle" onclick="togglePromptPreview(this)">Show/Hide</span>
194
- <div class="prompt-preview"></div>
195
- </label>
196
- <label class="radio-option">
197
- <input type="radio" name="prompt-selection" value="prompt3">
198
- Prompt 3
199
- <span class="prompt-toggle" onclick="togglePromptPreview(this)">Show/Hide</span>
200
- <div class="prompt-preview"></div>
201
- </label>
202
- <label class="radio-option">
203
- <input type="radio" name="prompt-selection" value="custom">
204
- Custom Prompt
205
- </label>
206
- </div>
207
- <div id="custom-prompt-container" style="margin-top: 0.5rem; display: none;">
208
- <textarea id="custom-prompt" placeholder="Enter custom instructions for the AI assistant" rows="3"></textarea>
209
- </div>
210
  </div>
211
  </div>
212
  </div>
@@ -223,57 +242,55 @@
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 => {
232
- const promptKey = preview.parentElement.querySelector('input[type="radio"]').value;
233
- if (SYSTEM_PROMPTS[promptKey]) {
234
- preview.textContent = SYSTEM_PROMPTS[promptKey];
235
- }
236
- });
237
- }
238
-
239
- function togglePromptPreview(element) {
240
- const preview = element.nextElementSibling;
241
- if (preview.style.display === 'block') {
242
- preview.style.display = 'none';
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;
252
 
253
  if (selectedValue === 'custom') {
254
  customPromptContainer.style.display = 'block';
 
255
  } else {
256
  customPromptContainer.style.display = 'none';
 
 
 
 
 
 
257
  }
258
- }
259
 
260
- function initRadioListeners() {
261
- const radioButtons = document.querySelectorAll('input[name="prompt-selection"]');
262
- radioButtons.forEach(radio => {
263
- radio.addEventListener('change', handlePromptSelection);
264
- });
265
  }
266
 
267
- document.addEventListener('DOMContentLoaded', function() {
268
- initPromptPreviews();
269
- initRadioListeners();
270
- });
 
 
 
 
271
 
272
  let peerConnection;
273
  let audioContext;
274
  let dataChannel;
275
  let isRecording = false;
276
  let webrtc_id;
 
 
277
  let animationId;
278
 
279
  const startButton = document.getElementById('start-button');
@@ -281,60 +298,65 @@
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;
315
-
316
- return {
317
- promptKey: selectedValue === 'custom' ? '' : selectedValue,
318
- customPrompt: selectedValue === 'custom' ? customPrompt : ''
319
- };
320
- }
321
-
322
  async function setupWebRTC() {
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,36 +368,32 @@
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,10 +417,22 @@
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,22 +442,34 @@
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,7 +482,6 @@
440
  }
441
  updateButtonState();
442
  }
443
-
444
  startButton.addEventListener('click', () => {
445
  if (!isRecording) {
446
  setupWebRTC();
@@ -453,4 +494,5 @@
453
  });
454
  </script>
455
  </body>
 
456
  </html>
 
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
  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
  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
  background-color: #ffd700;
159
  color: black;
160
  }
161
+ /* System prompt styles */
162
+ .prompt-selection {
163
+ border: 1px solid rgba(255, 255, 255, 0.1);
164
+ border-radius: 0.5rem;
165
+ padding: 1rem;
166
+ background-color: rgba(0, 0, 0, 0.1);
167
+ margin-bottom: 1rem;
 
 
 
 
168
  }
169
  .prompt-preview {
170
  padding: 0.5rem;
 
172
  margin-top: 0.25rem;
173
  background-color: rgba(0, 0, 0, 0.2);
174
  border-radius: 0.25rem;
175
+ max-height: 60px;
176
  overflow-y: auto;
177
  display: none;
178
  }
 
185
  }
186
  </style>
187
  </head>
188
+
189
  <body>
190
+ <!-- Add toast element after body opening tag -->
191
  <div id="error-toast" class="toast"></div>
192
  <div style="text-align: center">
193
  <h1>Gemini Voice Chat</h1>
194
  <p>Speak with Gemini using real-time audio streaming</p>
195
+ <p>
196
+ Get a Gemini API key
197
+ <a href="https://ai.google.dev/gemini-api/docs/api-key">here</a>
198
+ </p>
199
  </div>
200
  <div class="container">
201
  <div class="controls">
 
216
 
217
  <div class="input-group">
218
  <label>System Prompt</label>
219
+ <select id="prompt-select">
220
+ <option value="default">Default</option>
221
+ <option value="Behavior Analyst">Behavior Analyst</option>
222
+ <option value="UDL Expert">UDL Expert</option>
223
+ <option value="prompt3">Prompt 3</option>
224
+ <option value="custom">Custom Prompt</option>
225
+ </select>
226
+ <div id="prompt-preview" class="prompt-preview"></div>
227
+ <div id="custom-prompt-container" style="margin-top: 0.5rem; display: none;">
228
+ <textarea id="custom-prompt" placeholder="Enter custom instructions for the AI assistant" rows="3"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  </div>
230
  </div>
231
  </div>
 
242
  <audio id="audio-output"></audio>
243
 
244
  <script>
245
+ // System prompts data injected from the server
246
  const SYSTEM_PROMPTS = __SYSTEM_PROMPTS__;
247
 
248
+ // Initialize prompt elements
249
+ const promptSelect = document.getElementById('prompt-select');
250
+ const promptPreview = document.getElementById('prompt-preview');
251
+ const customPromptContainer = document.getElementById('custom-prompt-container');
252
+ const customPrompt = document.getElementById('custom-prompt');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ // Update prompt preview when selection changes
255
+ promptSelect.addEventListener('change', function() {
256
+ const selectedValue = promptSelect.value;
257
 
258
  if (selectedValue === 'custom') {
259
  customPromptContainer.style.display = 'block';
260
+ promptPreview.style.display = 'none';
261
  } else {
262
  customPromptContainer.style.display = 'none';
263
+ if (SYSTEM_PROMPTS[selectedValue]) {
264
+ promptPreview.textContent = SYSTEM_PROMPTS[selectedValue];
265
+ promptPreview.style.display = 'block';
266
+ } else {
267
+ promptPreview.style.display = 'none';
268
+ }
269
  }
270
+ });
271
 
272
+ // Show initial preview
273
+ if (SYSTEM_PROMPTS[promptSelect.value]) {
274
+ promptPreview.textContent = SYSTEM_PROMPTS[promptSelect.value];
275
+ promptPreview.style.display = 'block';
 
276
  }
277
 
278
+ // Get the currently selected prompt
279
+ function getSelectedPrompt() {
280
+ const selectedValue = promptSelect.value;
281
+ return {
282
+ promptKey: selectedValue === 'custom' ? '' : selectedValue,
283
+ customPrompt: selectedValue === 'custom' ? customPrompt.value : ''
284
+ };
285
+ }
286
 
287
  let peerConnection;
288
  let audioContext;
289
  let dataChannel;
290
  let isRecording = false;
291
  let webrtc_id;
292
+ let analyser;
293
+ let dataArray;
294
  let animationId;
295
 
296
  const startButton = document.getElementById('start-button');
 
298
  const voiceSelect = document.getElementById('voice');
299
  const audioOutput = document.getElementById('audio-output');
300
  const boxContainer = document.querySelector('.box-container');
 
301
  const numBars = 32;
302
  for (let i = 0; i < numBars; i++) {
303
  const box = document.createElement('div');
304
  box.className = 'box';
305
  boxContainer.appendChild(box);
306
  }
 
307
  function updateButtonState() {
308
  if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
309
+ startButton.innerHTML = `
310
+ <div class="icon-with-spinner">
311
+ <div class="spinner"></div>
312
+ <span>Connecting...</span>
313
+ </div>
314
+ `;
315
  } else if (peerConnection && peerConnection.connectionState === 'connected') {
316
+ startButton.innerHTML = `
317
+ <div class="pulse-container">
318
+ <div class="pulse-circle"></div>
319
+ <span>Stop Recording</span>
320
+ </div>
321
+ `;
322
  } else {
323
  startButton.innerHTML = 'Start Recording';
324
  }
325
  }
 
326
  function showError(message) {
327
  const toast = document.getElementById('error-toast');
328
  toast.textContent = message;
329
  toast.className = 'toast error';
330
  toast.style.display = 'block';
331
+ // Hide toast after 5 seconds
332
  setTimeout(() => {
333
  toast.style.display = 'none';
334
  }, 5000);
335
  }
 
 
 
 
 
 
 
 
 
 
 
336
  async function setupWebRTC() {
337
  const config = __RTC_CONFIGURATION__;
338
  peerConnection = new RTCPeerConnection(config);
339
  webrtc_id = Math.random().toString(36).substring(7);
340
+ const timeoutId = setTimeout(() => {
341
+ const toast = document.getElementById('error-toast');
342
+ toast.textContent = "Connection is taking longer than usual. Are you on a VPN?";
343
+ toast.className = 'toast warning';
344
+ toast.style.display = 'block';
345
+ // Hide warning after 5 seconds
346
+ setTimeout(() => {
347
+ toast.style.display = 'none';
348
+ }, 5000);
349
+ }, 5000);
350
  try {
351
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
352
  stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
353
+ // Update audio visualization setup
354
  audioContext = new AudioContext();
355
  const analyser_input = audioContext.createAnalyser();
356
  const source = audioContext.createMediaStreamSource(stream);
357
  source.connect(analyser_input);
358
  analyser_input.fftSize = 64;
359
  const dataArray_input = new Uint8Array(analyser_input.frequencyBinCount);
 
360
  function updateAudioLevel() {
361
  analyser_input.getByteFrequencyData(dataArray_input);
362
  const average = Array.from(dataArray_input).reduce((a, b) => a + b, 0) / dataArray_input.length;
 
368
  animationId = requestAnimationFrame(updateAudioLevel);
369
  }
370
  updateAudioLevel();
371
+ // Add connection state change listener
372
  peerConnection.addEventListener('connectionstatechange', () => {
373
+ console.log('connectionstatechange', peerConnection.connectionState);
374
+ if (peerConnection.connectionState === 'connected') {
375
+ clearTimeout(timeoutId);
376
+ const toast = document.getElementById('error-toast');
377
+ toast.style.display = 'none';
378
+ }
379
  updateButtonState();
380
  });
381
+ // Handle incoming audio
382
  peerConnection.addEventListener('track', (evt) => {
383
  if (audioOutput && audioOutput.srcObject !== evt.streams[0]) {
384
  audioOutput.srcObject = evt.streams[0];
385
  audioOutput.play();
386
+ // Set up audio visualization on the output stream
387
  audioContext = new AudioContext();
388
+ analyser = audioContext.createAnalyser();
389
  const source = audioContext.createMediaStreamSource(evt.streams[0]);
390
  source.connect(analyser);
391
  analyser.fftSize = 2048;
392
+ dataArray = new Uint8Array(analyser.frequencyBinCount);
 
 
 
 
 
 
 
 
 
 
393
  updateVisualization();
394
  }
395
  });
396
+ // Create data channel for messages
397
  dataChannel = peerConnection.createDataChannel('text');
398
  dataChannel.onmessage = (event) => {
399
  const eventJson = JSON.parse(event.data);
 
417
  });
418
  }
419
  };
420
+ // Create and send offer
421
  const offer = await peerConnection.createOffer();
422
  await peerConnection.setLocalDescription(offer);
423
+ await new Promise((resolve) => {
424
+ if (peerConnection.iceGatheringState === "complete") {
425
+ resolve();
426
+ } else {
427
+ const checkState = () => {
428
+ if (peerConnection.iceGatheringState === "complete") {
429
+ peerConnection.removeEventListener("icegatheringstatechange", checkState);
430
+ resolve();
431
+ }
432
+ };
433
+ peerConnection.addEventListener("icegatheringstatechange", checkState);
434
+ }
435
+ });
436
  const response = await fetch('/webrtc/offer', {
437
  method: 'POST',
438
  headers: { 'Content-Type': 'application/json' },
 
442
  webrtc_id: webrtc_id,
443
  })
444
  });
 
445
  const serverResponse = await response.json();
446
  if (serverResponse.status === 'failed') {
447
+ showError(serverResponse.meta.error === 'concurrency_limit_reached'
448
+ ? `Too many connections. Maximum limit is ${serverResponse.meta.limit}`
449
+ : serverResponse.meta.error);
450
  stopWebRTC();
451
+ startButton.textContent = 'Start Recording';
452
  return;
453
  }
 
454
  await peerConnection.setRemoteDescription(serverResponse);
455
  } catch (err) {
456
+ clearTimeout(timeoutId);
457
  console.error('Error setting up WebRTC:', err);
458
  showError('Failed to establish connection. Please try again.');
459
  stopWebRTC();
460
+ startButton.textContent = 'Start Recording';
461
  }
462
  }
463
+ function updateVisualization() {
464
+ if (!analyser) return;
465
+ analyser.getByteFrequencyData(dataArray);
466
+ const bars = document.querySelectorAll('.box');
467
+ for (let i = 0; i < bars.length; i++) {
468
+ const barHeight = (dataArray[i] / 255) * 2;
469
+ bars[i].style.transform = `scaleY(${Math.max(0.1, barHeight)})`;
470
+ }
471
+ animationId = requestAnimationFrame(updateVisualization);
472
+ }
473
  function stopWebRTC() {
474
  if (peerConnection) {
475
  peerConnection.close();
 
482
  }
483
  updateButtonState();
484
  }
 
485
  startButton.addEventListener('click', () => {
486
  if (!isRecording) {
487
  setupWebRTC();
 
494
  });
495
  </script>
496
  </body>
497
+
498
  </html>