YaTharThShaRma999 commited on
Commit
ed9ee9b
·
verified ·
1 Parent(s): e06b00c

Create test.html

Browse files
Files changed (1) hide show
  1. test.html +674 -0
test.html ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>Voice Assistant</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
9
+ <style>
10
+ /* Base styles for body - now the main canvas */
11
+ body {
12
+ font-family: 'Inter', sans-serif;
13
+ /* Soft, calming gradient background */
14
+ background: linear-gradient(135deg, #e0f7fa 0%, #e8f5e9 100%); /* Very light aqua to very light green */
15
+ display: flex;
16
+ flex-direction: column; /* Arrange content vertically */
17
+ justify-content: center;
18
+ align-items: center;
19
+ min-height: 100vh;
20
+ margin: 0;
21
+ padding: 20px;
22
+ box-sizing: border-box;
23
+ overflow: hidden; /* Prevent scrollbar from background */
24
+ position: relative;
25
+ gap: 2.5rem; /* Spacing between elements */
26
+ animation: fadeIn 0.9s ease-out forwards; /* Fade in the whole page */
27
+ }
28
+
29
+ @keyframes fadeIn {
30
+ from { opacity: 0; transform: translateY(30px); }
31
+ to { opacity: 1; transform: translateY(0); }
32
+ }
33
+
34
+ /* Animated background elements for a more dynamic look */
35
+ body::before, body::after {
36
+ content: '';
37
+ position: absolute;
38
+ border-radius: 50%;
39
+ opacity: 0.5; /* Softer opacity for background glows */
40
+ filter: blur(150px); /* Even more blur for a smoother, larger glow */
41
+ z-index: -1; /* Ensure they stay behind content */
42
+ }
43
+
44
+ body::before {
45
+ width: 450px;
46
+ height: 450px;
47
+ background: #4dd0e1; /* Cyan */
48
+ top: 5%;
49
+ left: 10%;
50
+ animation: moveBlob1 25s infinite alternate ease-in-out; /* Slower, calmer animation */
51
+ }
52
+
53
+ body::after {
54
+ width: 550px;
55
+ height: 550px;
56
+ background: #a5d6a7; /* Light Green */
57
+ bottom: 5%;
58
+ right: 8%;
59
+ animation: moveBlob2 28s infinite alternate-reverse ease-in-out; /* Slower, calmer animation */
60
+ }
61
+
62
+ @keyframes moveBlob1 {
63
+ 0% { transform: translate(0, 0) scale(1); }
64
+ 33% { transform: translate(70px, -50px) scale(1.08); }
65
+ 66% { transform: translate(-40px, 60px) scale(0.95); }
66
+ 100% { transform: translate(0, 0) scale(1); }
67
+ }
68
+
69
+ @keyframes moveBlob2 {
70
+ 0% { transform: translate(0, 0) scale(1); }
71
+ 33% { transform: translate(-60px, 80px) scale(0.9); }
72
+ 66% { transform: translate(50px, -70px) scale(1.1); }
73
+ 100% { transform: translate(0, 0) scale(1); }
74
+ }
75
+
76
+ /* Main title styling */
77
+ h1 {
78
+ color: #37474f; /* Dark text for contrast on light background */
79
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); /* Subtle text shadow */
80
+ margin-bottom: 0; /* Adjust spacing as body handles gap */
81
+ }
82
+
83
+ /* Status text styling */
84
+ .status-text {
85
+ font-size: 2rem; /* Even larger font for main status */
86
+ color: #546e7a; /* Muted dark gray for status */
87
+ font-weight: 700;
88
+ min-height: 3.5rem; /* Reserve more space to prevent CLS */
89
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); /* Subtle text shadow */
90
+ }
91
+
92
+ /* Microphone Button Styling */
93
+ .microphone-button {
94
+ width: 160px; /* Even larger button */
95
+ height: 160px;
96
+ border-radius: 50%;
97
+ /* Default: subtle, almost translucent white with a light inner glow */
98
+ background: rgba(255, 255, 255, 0.9); /* Very subtle transparency */
99
+ color: #546e7a; /* Muted gray for default icon */
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ font-size: 4rem; /* Larger icon */
104
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1), inset 0 0 20px rgba(255, 255, 255, 0.5); /* Stronger outer shadow, subtle inner glow */
105
+ cursor: pointer;
106
+ transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
107
+ position: relative;
108
+ overflow: hidden;
109
+ border: none;
110
+ margin: 0 auto; /* Center the button */
111
+ }
112
+ .microphone-button:hover {
113
+ transform: translateY(-5px) scale(1.05); /* More pronounced lift and grow */
114
+ box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15), inset 0 0 25px rgba(255, 255, 255, 0.7);
115
+ }
116
+ .microphone-button:active {
117
+ transform: translateY(0) scale(0.98);
118
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
119
+ }
120
+
121
+ /* Active states for microphone button */
122
+ .microphone-button.listening {
123
+ animation: pulse-active 2.5s infinite ease-in-out; /* Slower, more ethereal pulse */
124
+ background: linear-gradient(45deg, #26c6da, #80deea); /* Cyan gradient */
125
+ color: white; /* White icon when active */
126
+ box-shadow: 0 15px 30px rgba(38, 198, 218, 0.4), inset 0 0 30px rgba(255, 255, 255, 0.5); /* Soft cyan glow */
127
+ }
128
+ .microphone-button.speaking {
129
+ animation: pulse-active 2.5s infinite ease-in-out;
130
+ background: linear-gradient(45deg, #66bb6a, #81c784); /* Light green gradient */
131
+ color: white; /* White icon when active */
132
+ box-shadow: 0 15px 30px rgba(102, 187, 106, 0.4), inset 0 0 30px rgba(255, 255, 255, 0.5); /* Soft green glow */
133
+ }
134
+ .microphone-button.disabled {
135
+ opacity: 0.5;
136
+ cursor: not-allowed;
137
+ transform: none;
138
+ box-shadow: none;
139
+ background: #e0e0e0; /* Light gray for disabled */
140
+ color: #9e9e9e; /* Darker muted gray */
141
+ animation: none;
142
+ }
143
+
144
+ /* Specific pulse animation for active states */
145
+ @keyframes pulse-active {
146
+ 0% { transform: scale(1); box-shadow: 0 0 0 0 currentColor; }
147
+ 70% { transform: scale(1.08); box-shadow: 0 0 0 45px rgba(38, 198, 218, 0); } /* Uses currentColor for dynamic color */
148
+ 100% { transform: scale(1); box-shadow: 0 0 0 0 currentColor; }
149
+ }
150
+
151
+ /* SVG Icon styling */
152
+ .icon {
153
+ width: 4rem; /* Match button size */
154
+ height: 4rem;
155
+ fill: currentColor; /* Inherit color from parent button */
156
+ }
157
+
158
+ /* Toast Notification System */
159
+ #toastContainer {
160
+ position: fixed;
161
+ top: 20px;
162
+ right: 20px;
163
+ z-index: 1001;
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: 10px;
167
+ max-width: 300px;
168
+ }
169
+ .toast-message {
170
+ background-color: rgba(0, 0, 0, 0.75); /* Darker toast background for contrast */
171
+ color: white;
172
+ padding: 12px 20px;
173
+ border-radius: 8px;
174
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
175
+ opacity: 0;
176
+ transform: translateX(100%);
177
+ animation: slideIn 0.5s forwards, fadeOut 0.5s 2.5s forwards;
178
+ font-size: 0.9rem;
179
+ font-weight: 500;
180
+ }
181
+
182
+ @keyframes slideIn {
183
+ to { opacity: 1; transform: translateX(0); }
184
+ }
185
+ @keyframes fadeOut {
186
+ from { opacity: 1; }
187
+ to { opacity: 0; transform: translateX(100%); }
188
+ }
189
+
190
+ /* Custom Alert Modal (for critical errors) - adjusted for new light theme */
191
+ .modal {
192
+ position: fixed;
193
+ top: 0;
194
+ left: 0;
195
+ width: 100%;
196
+ height: 100%;
197
+ background-color: rgba(0, 0, 0, 0.6); /* Softer overlay */
198
+ display: flex;
199
+ justify-content: center;
200
+ align-items: center;
201
+ z-index: 1000;
202
+ opacity: 0;
203
+ visibility: hidden;
204
+ transition: opacity 0.4s ease, visibility 0.4s ease;
205
+ }
206
+ .modal.show {
207
+ opacity: 1;
208
+ visibility: visible;
209
+ }
210
+ .modal-content {
211
+ background-color: white; /* White modal content */
212
+ color: #37474f; /* Dark text */
213
+ padding: 3rem;
214
+ border-radius: 2rem;
215
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2); /* Softer shadow */
216
+ text-align: center;
217
+ max-width: 500px;
218
+ width: 90%;
219
+ transform: translateY(-50px) scale(0.9);
220
+ transition: transform 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55), opacity 0.4s ease;
221
+ /* No border */
222
+ }
223
+ .modal.show .modal-content {
224
+ transform: translateY(0) scale(1);
225
+ }
226
+ .modal-button {
227
+ background: linear-gradient(45deg, #4dd0e1, #26c6da); /* Calming blue/cyan gradient for modal button */
228
+ color: white;
229
+ padding: 0.8rem 1.8rem;
230
+ border-radius: 1rem;
231
+ margin-top: 2rem;
232
+ cursor: pointer;
233
+ transition: all 0.3s ease;
234
+ font-weight: 600;
235
+ border: none;
236
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
237
+ }
238
+ .modal-button:hover {
239
+ background: linear-gradient(45deg, #00acc1, #4dd0e1);
240
+ transform: translateY(-2px);
241
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
242
+ }
243
+
244
+ /* New Styles for More Options panel */
245
+ .options-button {
246
+ position: fixed;
247
+ top: 20px;
248
+ left: 20px;
249
+ background: rgba(255, 255, 255, 0.8);
250
+ backdrop-filter: blur(10px);
251
+ padding: 0.75rem 1.25rem;
252
+ border-radius: 1.5rem;
253
+ font-weight: 600;
254
+ color: #37474f;
255
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
256
+ cursor: pointer;
257
+ transition: all 0.3s ease;
258
+ z-index: 10;
259
+ }
260
+
261
+ .options-button:hover {
262
+ background: white;
263
+ transform: translateY(-2px);
264
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
265
+ }
266
+
267
+ .settings-panel {
268
+ position: fixed;
269
+ top: 0;
270
+ left: 0;
271
+ width: 350px;
272
+ height: 100vh;
273
+ padding: 4rem 2.5rem;
274
+ background: rgba(255, 255, 255, 0.75);
275
+ backdrop-filter: blur(20px);
276
+ -webkit-backdrop-filter: blur(20px);
277
+ box-shadow: 5px 0 30px rgba(0, 0, 0, 0.2);
278
+ transform: translateX(-100%);
279
+ transition: transform 0.5s cubic-bezier(0.7, 0, 0.3, 1);
280
+ z-index: 9;
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: 2rem;
284
+ }
285
+
286
+ .settings-panel.open {
287
+ transform: translateX(0);
288
+ }
289
+ </style>
290
+ </head>
291
+ <body>
292
+ <h1 class="text-5xl font-extrabold mb-4 text-shadow-lg">Voice Assistant</h1>
293
+
294
+ <div class="flex flex-col items-center gap-2">
295
+ <p id="statusText" class="status-text">Tap to speak</p>
296
+ </div>
297
+
298
+ <button id="microphoneButton" class="microphone-button">
299
+ <svg class="icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
300
+ <path d="M12 2C10.9 2 10 2.9 10 4v8c0 1.1.9 2 2 2s2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 17.92V22h2v-2.08c3.61-.49 6.4-3.5 6.4-7.42h-2c0 3.03-2.47 5.5-5.5 5.5S6.6 15.53 6.6 12H4.6c0 3.92 2.79 6.93 6.4 7.42z"/>
301
+ </svg>
302
+ </button>
303
+
304
+ <audio id="audio_output_component_id" autoplay class="hidden"></audio>
305
+
306
+ <button id="optionsButton" class="options-button">More Options</button>
307
+
308
+ <div id="settingsPanel" class="settings-panel">
309
+ <h2 class="text-3xl font-extrabold text-gray-800">Settings</h2>
310
+ <div class="flex flex-col gap-4">
311
+ <h3 class="text-xl font-bold text-gray-600">Upload File</h3>
312
+ <p class="text-sm text-gray-500">Upload a file to be used by the assistant.</p>
313
+ <input type="file" id="fileInput" class="rounded-lg p-2 text-gray-700 bg-gray-100 border border-gray-300">
314
+ <button id="uploadButton" class="btn bg-gray-700 text-white p-3 rounded-xl transition hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed">Upload File</button>
315
+ </div>
316
+ <div id="fileUploadStatus" class="text-sm text-gray-700 mt-4"></div>
317
+ </div>
318
+
319
+ <div id="toastContainer"></div>
320
+
321
+ <div id="customAlertModal" class="modal">
322
+ <div class="modal-content">
323
+ <p id="alertMessage" class="text-2xl font-semibold"></p>
324
+ <button id="alertCloseButton" class="modal-button">OK</button>
325
+ </div>
326
+ </div>
327
+
328
+ <script type="module">
329
+ // Firebase imports
330
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
331
+ import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
332
+ import { getFirestore } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
333
+
334
+ // Global WebRTC variables
335
+ let pc; // RTCPeerConnection instance
336
+ let localStream; // User's microphone stream
337
+ let webrtc_id; // Unique ID for this WebRTC session
338
+ let isWebRTCConnected = false; // Track connection state
339
+ let isListening = false; // Track if microphone is active for listening
340
+
341
+ // ** Explicitly define the signaling server URL **
342
+ const SIGNALING_SERVER_URL = 'https://c71505dfda9a.ngrok-free.app'; // Base URL
343
+
344
+ // Firebase variables
345
+ let db;
346
+ let auth;
347
+ let userId; // userId will still be used internally for signaling
348
+
349
+ // ** Your Firebase configuration **
350
+ const firebaseConfig = {
351
+ apiKey: "AIzaSyBWv5gIXhyHMIzBCH5oFDlSsJhXWzov-ms",
352
+ authDomain: "test2-947ce.firebaseapp.com",
353
+ projectId: "test2-947ce",
354
+ storageBucket: "test2-947ce.firebasestorage.app",
355
+ messagingSenderId: "211407958561",
356
+ appId: "1:211407958561:web:ea24b5a4da5e1053a5e960",
357
+ measurementId: "G-673MR7TX2C"
358
+ };
359
+ const appId = firebaseConfig.appId;
360
+
361
+
362
+ // UI Elements
363
+ const microphoneButton = document.getElementById('microphoneButton');
364
+ const statusText = document.getElementById('statusText');
365
+ const audioOutputComponent = document.getElementById("audio_output_component_id");
366
+ const customAlertModal = document.getElementById('customAlertModal');
367
+ const alertMessage = document.getElementById('alertMessage');
368
+ const alertCloseButton = document.getElementById('alertCloseButton');
369
+ const toastContainer = document.getElementById('toastContainer');
370
+ const optionsButton = document.getElementById('optionsButton');
371
+ const settingsPanel = document.getElementById('settingsPanel');
372
+ const fileInput = document.getElementById('fileInput');
373
+ const uploadButton = document.getElementById('uploadButton');
374
+ const fileUploadStatus = document.getElementById('fileUploadStatus');
375
+
376
+ /**
377
+ * Displays a custom alert modal instead of window.alert().
378
+ * Used for critical errors that require user attention.
379
+ * @param {string} message The message to display.
380
+ */
381
+ function showAlert(message) {
382
+ alertMessage.textContent = message;
383
+ customAlertModal.classList.add('show');
384
+ }
385
+
386
+ // Close the custom alert modal
387
+ alertCloseButton.addEventListener('click', () => {
388
+ customAlertModal.classList.remove('show');
389
+ });
390
+
391
+ /**
392
+ * Displays a transient toast message in the top-right corner.
393
+ * @param {string} message The message to display.
394
+ * @param {number} duration The duration in milliseconds for the toast to be visible (default: 3000ms).
395
+ */
396
+ function showToast(message, duration = 3000) {
397
+ const toast = document.createElement('div');
398
+ toast.classList.add('toast-message');
399
+ toast.textContent = message;
400
+ toastContainer.appendChild(toast);
401
+
402
+ setTimeout(() => {
403
+ toast.remove();
404
+ }, duration);
405
+ }
406
+
407
+ /**
408
+ * Updates the main status text on the UI.
409
+ * @param {string} text The status message.
410
+ */
411
+ function updateStatus(text) {
412
+ statusText.textContent = text;
413
+ }
414
+
415
+ /**
416
+ * Initializes Firebase and authenticates the user.
417
+ */
418
+ async function initializeFirebase() {
419
+ try {
420
+ const app = initializeApp(firebaseConfig);
421
+ auth = getAuth(app);
422
+ db = getFirestore(app);
423
+
424
+ onAuthStateChanged(auth, async (user) => {
425
+ if (user) {
426
+ userId = user.uid;
427
+ // User ID is no longer displayed on UI, but kept for internal use.
428
+ } else {
429
+ try {
430
+ if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
431
+ await signInWithCustomToken(auth, __initial_auth_token);
432
+ } else {
433
+ await signInAnonymously(auth);
434
+ }
435
+ } catch (error) {
436
+ console.error("Firebase Auth Error:", error);
437
+ showAlert(`Firebase authentication failed: ${error.message}`);
438
+ }
439
+ }
440
+ });
441
+ } catch (error) {
442
+ console.error("Error initializing Firebase:", error);
443
+ showAlert(`Failed to initialize Firebase: ${error.message}`);
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Sets up the WebRTC peer connection.
449
+ */
450
+ async function startWebRTC() {
451
+ if (isWebRTCConnected) {
452
+ showToast('WebRTC is already connected.');
453
+ return;
454
+ }
455
+
456
+ try {
457
+ updateStatus('Connecting...');
458
+ microphoneButton.classList.add('disabled'); // Disable button during connection setup
459
+ showToast('Initializing connection...');
460
+
461
+ pc = new RTCPeerConnection({
462
+ iceServers: [
463
+ { urls: 'stun:stun.l.google.com:19302' }
464
+ ]
465
+ });
466
+
467
+ localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
468
+ showToast('Microphone access granted.');
469
+
470
+ localStream.getTracks().forEach((track) => {
471
+ pc.addTrack(track, localStream);
472
+ });
473
+
474
+ pc.addEventListener("track", (evt) => {
475
+ if (audioOutputComponent && audioOutputComponent.srcObject !== evt.streams[0]) {
476
+ audioOutputComponent.srcObject = evt.streams[0];
477
+ audioOutputComponent.play().then(() => {
478
+ showToast('Assistant is speaking...');
479
+ updateStatus('Speaking...');
480
+ microphoneButton.classList.remove('listening');
481
+ microphoneButton.classList.add('speaking');
482
+ }).catch(e => console.error("Error playing audio:", e));
483
+
484
+ // Listen for when audio finishes playing
485
+ audioOutputComponent.onended = () => {
486
+ updateStatus('Listening...');
487
+ microphoneButton.classList.remove('speaking');
488
+ microphoneButton.classList.add('listening');
489
+ showToast('Ready for your next command.');
490
+ };
491
+ }
492
+ });
493
+
494
+ const dataChannel = pc.createDataChannel("text");
495
+ dataChannel.onopen = () => showToast('Data channel opened.');
496
+ dataChannel.onclose = () => showToast('Data channel closed.');
497
+ dataChannel.onerror = (err) => showToast(`Data channel error: ${err.message}`, 5000);
498
+
499
+ webrtc_id = Math.random().toString(36).substring(7);
500
+
501
+ pc.onicecandidate = ({ candidate }) => {
502
+ if (candidate) {
503
+ fetch(`${SIGNALING_SERVER_URL}/webrtc/offer`, {
504
+ method: 'POST',
505
+ headers: { 'Content-Type': 'application/json' },
506
+ body: JSON.stringify({
507
+ candidate: candidate.toJSON(),
508
+ webrtc_id: webrtc_id,
509
+ type: "ice-candidate",
510
+ userId: userId
511
+ })
512
+ })
513
+ .then(response => {
514
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
515
+ return response.json();
516
+ })
517
+ .catch(error => showToast(`Error sending ICE candidate: ${error.message}`, 5000));
518
+ }
519
+ };
520
+
521
+ const offer = await pc.createOffer();
522
+ await pc.setLocalDescription(offer);
523
+
524
+ const response = await fetch(`${SIGNALING_SERVER_URL}/webrtc/offer`, {
525
+ method: 'POST',
526
+ headers: { 'Content-Type': 'application/json' },
527
+ body: JSON.stringify({
528
+ sdp: offer.sdp,
529
+ type: offer.type,
530
+ webrtc_id: webrtc_id,
531
+ userId: userId
532
+ })
533
+ });
534
+
535
+ const serverResponse = await response.json();
536
+ // FIX: Explicitly create RTCSessionDescription with type 'answer'
537
+ await pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: serverResponse.sdp }));
538
+
539
+ isWebRTCConnected = true;
540
+ microphoneButton.classList.remove('disabled');
541
+ microphoneButton.classList.add('listening'); // Indicate ready to listen
542
+ updateStatus('Listening...');
543
+ showToast('WebRTC connected. Tap to speak!');
544
+
545
+ pc.onconnectionstatechange = () => {
546
+ if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed' || pc.connectionState === 'closed') {
547
+ showToast('WebRTC connection lost or failed.', 5000);
548
+ stopWebRTC();
549
+ } else if (pc.connectionState === 'connected') {
550
+ updateStatus('Listening...');
551
+ microphoneButton.classList.add('listening');
552
+ microphoneButton.classList.remove('speaking');
553
+ }
554
+ };
555
+
556
+ } catch (error) {
557
+ console.error("Error setting up WebRTC:", error);
558
+ showAlert(`Failed to start WebRTC: ${error.message}. Please ensure your microphone is available and try again.`);
559
+ updateStatus('Error');
560
+ microphoneButton.classList.remove('disabled', 'listening', 'speaking');
561
+ stopWebRTC(); // Clean up if setup fails
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Stops the WebRTC peer connection.
567
+ */
568
+ function stopWebRTC() {
569
+ if (!isWebRTCConnected && !pc) {
570
+ showToast('WebRTC is already stopped.');
571
+ return;
572
+ }
573
+
574
+ updateStatus('Stopping...');
575
+ showToast('Stopping connection...');
576
+
577
+ if (localStream) {
578
+ localStream.getTracks().forEach(track => track.stop());
579
+ localStream = null;
580
+ }
581
+
582
+ if (pc && pc.connectionState !== 'closed') {
583
+ pc.close();
584
+ }
585
+ pc = null;
586
+
587
+ if (audioOutputComponent) {
588
+ audioOutputComponent.srcObject = null;
589
+ audioOutputComponent.pause();
590
+ audioOutputComponent.currentTime = 0;
591
+ }
592
+
593
+ isWebRTCConnected = false;
594
+ isListening = false;
595
+ microphoneButton.classList.remove('listening', 'speaking', 'disabled');
596
+ updateStatus('Tap to speak');
597
+ showToast('WebRTC connection ended.');
598
+ }
599
+
600
+ /**
601
+ * Toggles the WebRTC connection based on its current state.
602
+ */
603
+ async function toggleWebRTC() {
604
+ if (isWebRTCConnected) {
605
+ stopWebRTC();
606
+ } else {
607
+ await startWebRTC();
608
+ }
609
+ }
610
+
611
+ // Event Listener for the central microphone button
612
+ microphoneButton.addEventListener('click', toggleWebRTC);
613
+
614
+ // Options panel logic
615
+ optionsButton.addEventListener('click', () => {
616
+ settingsPanel.classList.toggle('open');
617
+ });
618
+
619
+ // File upload logic
620
+ uploadButton.addEventListener('click', async () => {
621
+ const file = fileInput.files[0];
622
+ if (!file) {
623
+ fileUploadStatus.textContent = 'Please select a file to upload.';
624
+ return;
625
+ }
626
+ if (!userId) {
627
+ fileUploadStatus.textContent = 'Authentication not ready, please wait.';
628
+ return;
629
+ }
630
+
631
+ fileUploadStatus.textContent = 'Uploading...';
632
+ uploadButton.disabled = true;
633
+
634
+ const reader = new FileReader();
635
+ reader.onload = async function(event) {
636
+ const base64String = event.target.result.split(',')[1];
637
+
638
+ try {
639
+ const response = await fetch(`${SIGNALING_SERVER_URL}/settings`, {
640
+ method: 'POST',
641
+ headers: { 'Content-Type': 'application/json' },
642
+ body: JSON.stringify({
643
+ userId: userId,
644
+ fileName: file.name,
645
+ fileType: file.type,
646
+ voice_cloning_file: base64String // Changed key name here
647
+ })
648
+ });
649
+
650
+ if (response.ok) {
651
+ fileUploadStatus.textContent = `File "${file.name}" uploaded successfully!`;
652
+ fileInput.value = ''; // Clear the file input
653
+ showToast('Settings updated with file upload!');
654
+ } else {
655
+ const errorData = await response.json();
656
+ fileUploadStatus.textContent = `Upload failed: ${errorData.error}`;
657
+ showToast('File upload failed.', 5000);
658
+ }
659
+ } catch (error) {
660
+ fileUploadStatus.textContent = `An error occurred: ${error.message}`;
661
+ showToast('An error occurred during upload.', 5000);
662
+ } finally {
663
+ uploadButton.disabled = false;
664
+ }
665
+ };
666
+ reader.readAsDataURL(file);
667
+ });
668
+
669
+ // Initialize Firebase when the window loads
670
+ window.onload = initializeFirebase;
671
+
672
+ </script>
673
+ </body>
674
+ </html>