Pepguy commited on
Commit
5e051aa
·
verified ·
1 Parent(s): e3293e2

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +928 -78
index.html CHANGED
@@ -1,78 +1,928 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
-
18
- <button id="clickme"> Stop </button>
19
- </div>
20
-
21
- <script type="module">
22
- // Import the functions you need from the SDKs you need
23
- import { initializeApp } from "https://www.gstatic.com/firebasejs/12.3.0/firebase-app.js";
24
- import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.3.0/firebase-analytics.js";
25
- // TODO: Add SDKs for Firebase products that you want to use
26
- // https://firebase.google.com/docs/web/setup#available-libraries
27
-
28
- // Your web app's Firebase configuration
29
- // For Firebase JS SDK v7.20.0 and later, measurementId is optional
30
- const firebaseConfig = {
31
- apiKey: "AIzaSyB6EeMqhn5kvMEDYRzhtdokA9i0idKtUKo",
32
- authDomain: "winternut-pay.firebaseapp.com",
33
- projectId: "winternut-pay",
34
- storageBucket: "winternut-pay.firebasestorage.app",
35
- messagingSenderId: "564557307443",
36
- appId: "1:564557307443:web:a1abf6b0db99ca79560d46",
37
- measurementId: "G-K07BVBE7FW"
38
- };
39
-
40
- // Initialize Firebase
41
- const app = initializeApp(firebaseConfig);
42
- const analytics = getAnalytics(app);
43
-
44
-
45
-
46
-
47
- // Initialize the Vertex AI Gemini API backend service
48
- // Set the location to `us-central1` (the flash-live model is only supported in that location)
49
- const ai = getAI(firebaseApp, { backend: new VertexAIBackend("us-central1") });
50
-
51
- // Create a `LiveGenerativeModel` instance with the flash-live model (only model that supports the Live API)
52
- const model = getLiveGenerativeModel(ai, {
53
- model: "gemini-2.0-flash-live-preview-04-09",
54
- // Configure the model to respond with audio
55
- generationConfig: {
56
- responseModalities: [ResponseModality.AUDIO],
57
- },
58
- });
59
-
60
- const session = await model.connect();
61
-
62
- // Start the audio conversation
63
- const audioConversationController = await startAudioConversation(session);
64
-
65
- // ... Later, to stop the audio conversation
66
- // await audioConversationController.stop()
67
-
68
-
69
- // ... Later, to stop the audio conversation
70
- // await audioConversationController.stop()
71
- document.getElementById("clickme").onclick( async ()=> {
72
- await audioConversationController.stop()
73
- })
74
- </script>
75
-
76
-
77
- </body>
78
- </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">
6
+ <title>Firebase Gemini Live API Chat</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ padding: 20px;
22
+ }
23
+
24
+ .container {
25
+ width: 100%;
26
+ max-width: 800px;
27
+ background: white;
28
+ border-radius: 20px;
29
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
30
+ overflow: hidden;
31
+ display: flex;
32
+ flex-direction: column;
33
+ height: 90vh;
34
+ max-height: 700px;
35
+ }
36
+
37
+ .header {
38
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
39
+ color: white;
40
+ padding: 20px;
41
+ text-align: center;
42
+ }
43
+
44
+ .header h1 {
45
+ font-size: 24px;
46
+ margin-bottom: 5px;
47
+ }
48
+
49
+ .status {
50
+ font-size: 14px;
51
+ opacity: 0.9;
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ gap: 8px;
56
+ }
57
+
58
+ .status-dot {
59
+ width: 8px;
60
+ height: 8px;
61
+ border-radius: 50%;
62
+ background: #ff4444;
63
+ animation: pulse 2s infinite;
64
+ }
65
+
66
+ .status-dot.connected {
67
+ background: #44ff44;
68
+ }
69
+
70
+ @keyframes pulse {
71
+ 0%, 100% { opacity: 1; }
72
+ 50% { opacity: 0.5; }
73
+ }
74
+
75
+ .config-panel {
76
+ background: #f8f9fa;
77
+ padding: 15px;
78
+ border-bottom: 1px solid #e0e0e0;
79
+ }
80
+
81
+ .config-row {
82
+ display: flex;
83
+ gap: 10px;
84
+ margin-bottom: 10px;
85
+ }
86
+
87
+ .config-row input,
88
+ .config-row select,
89
+ .config-row button {
90
+ padding: 8px 12px;
91
+ border: 1px solid #ddd;
92
+ border-radius: 8px;
93
+ font-size: 14px;
94
+ }
95
+
96
+ .config-row input {
97
+ flex: 1;
98
+ }
99
+
100
+ .config-row button {
101
+ background: #667eea;
102
+ color: white;
103
+ border: none;
104
+ cursor: pointer;
105
+ font-weight: 600;
106
+ transition: background 0.3s;
107
+ }
108
+
109
+ .config-row button:hover {
110
+ background: #5568d3;
111
+ }
112
+
113
+ .config-row button:disabled {
114
+ background: #ccc;
115
+ cursor: not-allowed;
116
+ }
117
+
118
+ .mode-toggle {
119
+ display: flex;
120
+ gap: 5px;
121
+ }
122
+
123
+ .mode-toggle button {
124
+ flex: 1;
125
+ padding: 8px;
126
+ border: 2px solid #667eea;
127
+ background: white;
128
+ color: #667eea;
129
+ border-radius: 8px;
130
+ cursor: pointer;
131
+ font-size: 11px;
132
+ font-weight: 600;
133
+ transition: all 0.3s;
134
+ }
135
+
136
+ .mode-toggle button.active {
137
+ background: #667eea;
138
+ color: white;
139
+ }
140
+
141
+ .chat-messages {
142
+ flex: 1;
143
+ overflow-y: auto;
144
+ padding: 20px;
145
+ background: #f8f9fa;
146
+ }
147
+
148
+ .message {
149
+ margin-bottom: 15px;
150
+ display: flex;
151
+ animation: slideIn 0.3s ease-out;
152
+ }
153
+
154
+ @keyframes slideIn {
155
+ from {
156
+ opacity: 0;
157
+ transform: translateY(10px);
158
+ }
159
+ to {
160
+ opacity: 1;
161
+ transform: translateY(0);
162
+ }
163
+ }
164
+
165
+ .message.user {
166
+ justify-content: flex-end;
167
+ }
168
+
169
+ .message-content {
170
+ max-width: 70%;
171
+ padding: 12px 16px;
172
+ border-radius: 18px;
173
+ word-wrap: break-word;
174
+ }
175
+
176
+ .message.user .message-content {
177
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
178
+ color: white;
179
+ border-bottom-right-radius: 4px;
180
+ }
181
+
182
+ .message.assistant .message-content {
183
+ background: white;
184
+ color: #333;
185
+ border-bottom-left-radius: 4px;
186
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
187
+ }
188
+
189
+ .message.system .message-content {
190
+ background: #fff3cd;
191
+ color: #856404;
192
+ max-width: 100%;
193
+ text-align: center;
194
+ font-size: 13px;
195
+ }
196
+
197
+ .input-area {
198
+ padding: 20px;
199
+ background: white;
200
+ border-top: 1px solid #e0e0e0;
201
+ }
202
+
203
+ .input-row {
204
+ display: flex;
205
+ gap: 10px;
206
+ align-items: center;
207
+ }
208
+
209
+ .input-row input {
210
+ flex: 1;
211
+ padding: 12px 16px;
212
+ border: 2px solid #e0e0e0;
213
+ border-radius: 25px;
214
+ font-size: 14px;
215
+ outline: none;
216
+ transition: border-color 0.3s;
217
+ }
218
+
219
+ .input-row input:focus {
220
+ border-color: #667eea;
221
+ }
222
+
223
+ .btn {
224
+ padding: 12px 24px;
225
+ border: none;
226
+ border-radius: 25px;
227
+ font-size: 14px;
228
+ font-weight: 600;
229
+ cursor: pointer;
230
+ transition: all 0.3s;
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 8px;
234
+ }
235
+
236
+ .btn-primary {
237
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
238
+ color: white;
239
+ }
240
+
241
+ .btn-primary:hover:not(:disabled) {
242
+ transform: translateY(-2px);
243
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
244
+ }
245
+
246
+ .btn-secondary {
247
+ background: #6c757d;
248
+ color: white;
249
+ }
250
+
251
+ .btn-secondary:hover:not(:disabled) {
252
+ background: #5a6268;
253
+ }
254
+
255
+ .btn-voice {
256
+ width: 50px;
257
+ height: 50px;
258
+ border-radius: 50%;
259
+ padding: 0;
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: center;
263
+ }
264
+
265
+ .btn-voice.recording {
266
+ background: #ff4444;
267
+ animation: recordPulse 1s infinite;
268
+ }
269
+
270
+ @keyframes recordPulse {
271
+ 0%, 100% { transform: scale(1); }
272
+ 50% { transform: scale(1.1); }
273
+ }
274
+
275
+ .btn:disabled {
276
+ opacity: 0.5;
277
+ cursor: not-allowed;
278
+ }
279
+
280
+ .typing-indicator {
281
+ display: flex;
282
+ gap: 4px;
283
+ padding: 12px 16px;
284
+ }
285
+
286
+ .typing-dot {
287
+ width: 8px;
288
+ height: 8px;
289
+ border-radius: 50%;
290
+ background: #999;
291
+ animation: typing 1.4s infinite;
292
+ }
293
+
294
+ .typing-dot:nth-child(2) {
295
+ animation-delay: 0.2s;
296
+ }
297
+
298
+ .typing-dot:nth-child(3) {
299
+ animation-delay: 0.4s;
300
+ }
301
+
302
+ @keyframes typing {
303
+ 0%, 60%, 100% {
304
+ transform: translateY(0);
305
+ }
306
+ 30% {
307
+ transform: translateY(-10px);
308
+ }
309
+ }
310
+
311
+ .audio-visualizer {
312
+ display: flex;
313
+ gap: 3px;
314
+ align-items: center;
315
+ justify-content: center;
316
+ height: 30px;
317
+ }
318
+
319
+ .visualizer-bar {
320
+ width: 3px;
321
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
322
+ border-radius: 2px;
323
+ animation: visualize 0.5s ease-in-out infinite alternate;
324
+ }
325
+
326
+ .visualizer-bar:nth-child(1) { animation-delay: 0s; }
327
+ .visualizer-bar:nth-child(2) { animation-delay: 0.1s; }
328
+ .visualizer-bar:nth-child(3) { animation-delay: 0.2s; }
329
+ .visualizer-bar:nth-child(4) { animation-delay: 0.3s; }
330
+ .visualizer-bar:nth-child(5) { animation-delay: 0.2s; }
331
+ .visualizer-bar:nth-child(6) { animation-delay: 0.1s; }
332
+
333
+ @keyframes visualize {
334
+ 0% { height: 5px; }
335
+ 100% { height: 25px; }
336
+ }
337
+ </style>
338
+ </head>
339
+ <body>
340
+ <div class="container">
341
+ <div class="header">
342
+ <h1>🤖 Gemini Live Chat</h1>
343
+ <div class="status">
344
+ <div class="status-dot" id="statusDot"></div>
345
+ <span id="statusText">Disconnected</span>
346
+ </div>
347
+ </div>
348
+
349
+ <div class="config-panel">
350
+ <div class="config-row">
351
+ <input type="text" id="apiKey" placeholder="Enter your Firebase API Key" />
352
+ <input type="text" id="projectId" placeholder="Firebase Project ID" />
353
+ <button id="connectBtn" onclick="connect()">Connect</button>
354
+ </div>
355
+ <div class="config-row">
356
+ <select id="modelSelect">
357
+ <option value="gemini-2.0-flash-live-preview-04-09">gemini-2.0-flash-live-preview-04-09</option>
358
+ <option value="gemini-live-2.5-flash">gemini-live-2.5-flash</option>
359
+ </select>
360
+ <div class="mode-toggle">
361
+ <button class="active" onclick="setMode('text')">Text → Text</button>
362
+ <button onclick="setMode('audio-audio')">Audio → Audio</button>
363
+ <button onclick="setMode('speech-audio')">Speech → Audio</button>
364
+ </div>
365
+ </div>
366
+ </div>
367
+
368
+ <div class="chat-messages" id="chatMessages">
369
+ <div class="message system">
370
+ <div class="message-content">
371
+ 👋 Welcome! Configure your Firebase credentials and click Connect to start chatting.
372
+ <br><br>
373
+ <strong>Modes:</strong><br>
374
+ • Text → Text: Type and receive text<br>
375
+ • Audio → Audio: TTS input, Gemini audio output<br>
376
+ • Speech → Audio: Browser speech recognition, Gemini audio output
377
+ </div>
378
+ </div>
379
+ </div>
380
+
381
+ <div class="input-area">
382
+ <div class="input-row" id="textInputRow">
383
+ <input
384
+ type="text"
385
+ id="messageInput"
386
+ placeholder="Type your message..."
387
+ disabled
388
+ onkeypress="if(event.key === 'Enter') sendMessage()"
389
+ />
390
+ <button class="btn btn-primary" id="sendBtn" onclick="sendMessage()" disabled>
391
+ Send
392
+ </button>
393
+ <button class="btn btn-secondary" id="disconnectBtn" onclick="disconnect()" disabled>
394
+ Disconnect
395
+ </button>
396
+ </div>
397
+ <div class="input-row" id="voiceInputRow" style="display: none;">
398
+ <button class="btn btn-voice btn-primary" id="voiceBtn" onclick="toggleVoiceInput()" disabled>
399
+ 🎤
400
+ </button>
401
+ <div id="audioVisualizer" style="display: none;">
402
+ <div class="audio-visualizer">
403
+ <div class="visualizer-bar"></div>
404
+ <div class="visualizer-bar"></div>
405
+ <div class="visualizer-bar"></div>
406
+ <div class="visualizer-bar"></div>
407
+ <div class="visualizer-bar"></div>
408
+ <div class="visualizer-bar"></div>
409
+ </div>
410
+ </div>
411
+ <button class="btn btn-secondary" id="disconnectBtn2" onclick="disconnect()" disabled>
412
+ Disconnect
413
+ </button>
414
+ </div>
415
+ </div>
416
+ </div>
417
+
418
+ <script type="module">
419
+ import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js';
420
+ import { getAI, getLiveGenerativeModel } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-ai-logic.js';
421
+ import { VertexAIBackend } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-ai-logic.js';
422
+
423
+ let firebaseApp = null;
424
+ let session = null;
425
+ let currentMode = 'text';
426
+ let isReceiving = false;
427
+ let recognition = null;
428
+ let audioContext = null;
429
+ let isRecording = false;
430
+ let mediaRecorder = null;
431
+ let audioChunks = [];
432
+
433
+ // Make functions globally available
434
+ window.connect = connect;
435
+ window.disconnect = disconnect;
436
+ window.sendMessage = sendMessage;
437
+ window.setMode = setMode;
438
+ window.toggleVoiceInput = toggleVoiceInput;
439
+
440
+ const ResponseModality = {
441
+ TEXT: 'text',
442
+ AUDIO: 'audio'
443
+ };
444
+
445
+ // Initialize Web Speech API
446
+ if ('webkitSpeechRecognition' in window) {
447
+ recognition = new webkitSpeechRecognition();
448
+ recognition.continuous = false;
449
+ recognition.interimResults = false;
450
+ recognition.lang = 'en-US';
451
+
452
+ recognition.onresult = (event) => {
453
+ const transcript = event.results[0][0].transcript;
454
+ addMessage('user', transcript);
455
+ sendTextMessage(transcript);
456
+ };
457
+
458
+ recognition.onerror = (event) => {
459
+ console.error('Speech recognition error:', event.error);
460
+ addMessage('system', `❌ Speech recognition error: ${event.error}`);
461
+ stopRecording();
462
+ };
463
+
464
+ recognition.onend = () => {
465
+ stopRecording();
466
+ };
467
+ }
468
+
469
+ async function connect() {
470
+ const apiKey = document.getElementById('apiKey').value.trim();
471
+ const projectId = document.getElementById('projectId').value.trim();
472
+ const modelName = document.getElementById('modelSelect').value;
473
+
474
+ if (!apiKey || !projectId) {
475
+ addMessage('system', 'Please enter both API Key and Project ID');
476
+ return;
477
+ }
478
+
479
+ try {
480
+ document.getElementById('connectBtn').disabled = true;
481
+ addMessage('system', 'Connecting to Firebase...');
482
+
483
+ const firebaseConfig = {
484
+ apiKey: apiKey,
485
+ projectId: projectId
486
+ };
487
+
488
+ firebaseApp = initializeApp(firebaseConfig);
489
+ const ai = getAI(firebaseApp, {
490
+ backend: new VertexAIBackend("us-central1")
491
+ });
492
+
493
+ const useAudio = currentMode !== 'text';
494
+ const model = getLiveGenerativeModel(ai, {
495
+ model: modelName,
496
+ generationConfig: {
497
+ responseModalities: [useAudio ? ResponseModality.AUDIO : ResponseModality.TEXT],
498
+ }
499
+ });
500
+
501
+ session = await model.connect();
502
+
503
+ updateStatus(true);
504
+ addMessage('system', `✅ Connected in ${currentMode} mode!`);
505
+
506
+ // Enable appropriate inputs
507
+ if (currentMode === 'text') {
508
+ document.getElementById('messageInput').disabled = false;
509
+ document.getElementById('sendBtn').disabled = false;
510
+ } else {
511
+ document.getElementById('voiceBtn').disabled = false;
512
+ }
513
+
514
+ document.getElementById('disconnectBtn').disabled = false;
515
+ document.getElementById('disconnectBtn2').disabled = false;
516
+ document.getElementById('connectBtn').disabled = true;
517
+
518
+ // Initialize audio context for audio playback
519
+ if (useAudio) {
520
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
521
+ }
522
+
523
+ startReceiving();
524
+
525
+ } catch (error) {
526
+ console.error('Connection error:', error);
527
+ addMessage('system', `❌ Connection failed: ${error.message}`);
528
+ document.getElementById('connectBtn').disabled = false;
529
+ }
530
+ }
531
+
532
+ async function disconnect() {
533
+ if (session) {
534
+ try {
535
+ await session.close();
536
+ } catch (e) {
537
+ console.error('Error closing session:', e);
538
+ }
539
+ session = null;
540
+ }
541
+
542
+ if (audioContext) {
543
+ audioContext.close();
544
+ audioContext = null;
545
+ }
546
+
547
+ updateStatus(false);
548
+ addMessage('system', 'Disconnected from server');
549
+
550
+ document.getElementById('messageInput').disabled = true;
551
+ document.getElementById('sendBtn').disabled = true;
552
+ document.getElementById('voiceBtn').disabled = true;
553
+ document.getElementById('disconnectBtn').disabled = true;
554
+ document.getElementById('disconnectBtn2').disabled = true;
555
+ document.getElementById('connectBtn').disabled = false;
556
+ }
557
+
558
+ async function sendMessage() {
559
+ const input = document.getElementById('messageInput');
560
+ const message = input.value.trim();
561
+
562
+ if (!message || !session) return;
563
+
564
+ addMessage('user', message);
565
+ input.value = '';
566
+
567
+ await sendTextMessage(message);
568
+ }
569
+
570
+ async function sendTextMessage(text) {
571
+ showTypingIndicator();
572
+
573
+ try {
574
+ // For audio-audio mode, use TTS to convert text to audio
575
+ if (currentMode === 'audio-audio') {
576
+ const audioData = await textToSpeech(text);
577
+ await session.send([{
578
+ inlineData: {
579
+ data: audioData,
580
+ mimeType: "audio/pcm"
581
+ }
582
+ }]);
583
+ } else {
584
+ await session.send(text);
585
+ }
586
+ } catch (error) {
587
+ console.error('Send error:', error);
588
+ hideTypingIndicator();
589
+ addMessage('system', `❌ Error sending message: ${error.message}`);
590
+ }
591
+ }
592
+
593
+ async function textToSpeech(text) {
594
+ return new Promise((resolve, reject) => {
595
+ const utterance = new SpeechSynthesisUtterance(text);
596
+
597
+ // Create an audio context to capture the speech
598
+ const tempAudioContext = new AudioContext({ sampleRate: 16000 });
599
+ const destination = tempAudioContext.createMediaStreamDestination();
600
+
601
+ utterance.onend = () => {
602
+ // This is a simplified version - in production, you'd need to
603
+ // properly capture and convert the audio to PCM format
604
+ const buffer = new ArrayBuffer(text.length * 32);
605
+ const view = new Uint8Array(buffer);
606
+
607
+ // Convert to base64
608
+ const base64 = btoa(String.fromCharCode(...view));
609
+ resolve(base64);
610
+ };
611
+
612
+ utterance.onerror = (error) => {
613
+ reject(error);
614
+ };
615
+
616
+ speechSynthesis.speak(utterance);
617
+ });
618
+ }
619
+
620
+ function toggleVoiceInput() {
621
+ if (isRecording) {
622
+ stopRecording();
623
+ } else {
624
+ startRecording();
625
+ }
626
+ }
627
+
628
+ function startRecording() {
629
+ const voiceBtn = document.getElementById('voiceBtn');
630
+ const visualizer = document.getElementById('audioVisualizer');
631
+
632
+ if (currentMode === 'speech-audio') {
633
+ // Use browser speech recognition
634
+ if (recognition) {
635
+ recognition.start();
636
+ voiceBtn.classList.add('recording');
637
+ voiceBtn.textContent = '⏹️';
638
+ visualizer.style.display = 'block';
639
+ isRecording = true;
640
+ addMessage('system', '🎤 Listening...');
641
+ } else {
642
+ addMessage('system', '❌ Speech recognition not supported in this browser');
643
+ }
644
+ } else if (currentMode === 'audio-audio') {
645
+ // Use microphone for audio recording
646
+ navigator.mediaDevices.getUserMedia({ audio: true })
647
+ .then(stream => {
648
+ mediaRecorder = new MediaRecorder(stream);
649
+ audioChunks = [];
650
+
651
+ mediaRecorder.ondataavailable = (event) => {
652
+ audioChunks.push(event.data);
653
+ };
654
+
655
+ mediaRecorder.onstop = async () => {
656
+ const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
657
+ await processAudioBlob(audioBlob);
658
+ stream.getTracks().forEach(track => track.stop());
659
+ };
660
+
661
+ mediaRecorder.start();
662
+ voiceBtn.classList.add('recording');
663
+ voiceBtn.textContent = '⏹️';
664
+ visualizer.style.display = 'block';
665
+ isRecording = true;
666
+ addMessage('system', '🎤 Recording audio...');
667
+ })
668
+ .catch(error => {
669
+ console.error('Microphone error:', error);
670
+ addMessage('system', '❌ Microphone access denied');
671
+ });
672
+ }
673
+ }
674
+
675
+ function stopRecording() {
676
+ const voiceBtn = document.getElementById('voiceBtn');
677
+ const visualizer = document.getElementById('audioVisualizer');
678
+
679
+ if (currentMode === 'speech-audio' && recognition) {
680
+ recognition.stop();
681
+ } else if (currentMode === 'audio-audio' && mediaRecorder) {
682
+ mediaRecorder.stop();
683
+ }
684
+
685
+ voiceBtn.classList.remove('recording');
686
+ voiceBtn.textContent = '🎤';
687
+ visualizer.style.display = 'none';
688
+ isRecording = false;
689
+ }
690
+
691
+ async function processAudioBlob(blob) {
692
+ try {
693
+ const arrayBuffer = await blob.arrayBuffer();
694
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
695
+
696
+ // Convert to PCM 16-bit 16kHz
697
+ const pcmData = convertToPCM(audioBuffer);
698
+ const base64Data = arrayBufferToBase64(pcmData);
699
+
700
+ addMessage('user', '🎤 [Audio message]');
701
+ showTypingIndicator();
702
+
703
+ await session.send([{
704
+ inlineData: {
705
+ data: base64Data,
706
+ mimeType: "audio/pcm"
707
+ }
708
+ }]);
709
+ } catch (error) {
710
+ console.error('Audio processing error:', error);
711
+ addMessage('system', `❌ Error processing audio: ${error.message}`);
712
+ }
713
+ }
714
+
715
+ function convertToPCM(audioBuffer) {
716
+ const targetSampleRate = 16000;
717
+ const channelData = audioBuffer.getChannelData(0);
718
+ const samples = resampleAudio(channelData, audioBuffer.sampleRate, targetSampleRate);
719
+
720
+ const pcm = new Int16Array(samples.length);
721
+ for (let i = 0; i < samples.length; i++) {
722
+ const s = Math.max(-1, Math.min(1, samples[i]));
723
+ pcm[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
724
+ }
725
+
726
+ return pcm.buffer;
727
+ }
728
+
729
+ function resampleAudio(samples, fromRate, toRate) {
730
+ const ratio = fromRate / toRate;
731
+ const newLength = Math.round(samples.length / ratio);
732
+ const result = new Float32Array(newLength);
733
+
734
+ for (let i = 0; i < newLength; i++) {
735
+ const srcIndex = i * ratio;
736
+ const srcIndexFloor = Math.floor(srcIndex);
737
+ const srcIndexCeil = Math.min(srcIndexFloor + 1, samples.length - 1);
738
+ const t = srcIndex - srcIndexFloor;
739
+
740
+ result[i] = samples[srcIndexFloor] * (1 - t) + samples[srcIndexCeil] * t;
741
+ }
742
+
743
+ return result;
744
+ }
745
+
746
+ function arrayBufferToBase64(buffer) {
747
+ const bytes = new Uint8Array(buffer);
748
+ let binary = '';
749
+ for (let i = 0; i < bytes.byteLength; i++) {
750
+ binary += String.fromCharCode(bytes[i]);
751
+ }
752
+ return btoa(binary);
753
+ }
754
+
755
+ async function startReceiving() {
756
+ if (isReceiving || !session) return;
757
+
758
+ isReceiving = true;
759
+ let currentText = '';
760
+ let audioQueue = [];
761
+
762
+ try {
763
+ const messages = session.receive();
764
+
765
+ for await (const message of messages) {
766
+ hideTypingIndicator();
767
+
768
+ switch (message.type) {
769
+ case 'serverContent':
770
+ if (message.turnComplete) {
771
+ if (currentText) {
772
+ addMessage('assistant', currentText);
773
+ currentText = '';
774
+ }
775
+ if (audioQueue.length > 0) {
776
+ await playAudioChunks(audioQueue);
777
+ audioQueue = [];
778
+ }
779
+ } else if (message.modelTurn) {
780
+ const parts = message.modelTurn?.parts;
781
+ if (parts) {
782
+ parts.forEach(part => {
783
+ if (part.text) {
784
+ currentText += part.text;
785
+ } else if (part.inlineData) {
786
+ audioQueue.push(part.inlineData.data);
787
+ }
788
+ });
789
+ }
790
+ }
791
+ break;
792
+
793
+ case 'toolCall':
794
+ addMessage('system', '🔧 Model is calling a tool...');
795
+ break;
796
+
797
+ case 'toolCallCancellation':
798
+ addMessage('system', '⚠️ Tool call was cancelled');
799
+ break;
800
+ }
801
+ }
802
+ } catch (error) {
803
+ console.error('Receive error:', error);
804
+ addMessage('system', `❌ Error receiving messages: ${error.message}`);
805
+ } finally {
806
+ isReceiving = false;
807
+ }
808
+ }
809
+
810
+ async function playAudioChunks(chunks) {
811
+ if (!audioContext || chunks.length === 0) return;
812
+
813
+ addMessage('assistant', '🔊 [Audio response]');
814
+
815
+ for (const base64Data of chunks) {
816
+ try {
817
+ const binaryData = atob(base64Data);
818
+ const arrayBuffer = new ArrayBuffer(binaryData.length);
819
+ const view = new Uint8Array(arrayBuffer);
820
+
821
+ for (let i = 0; i < binaryData.length; i++) {
822
+ view[i] = binaryData.charCodeAt(i);
823
+ }
824
+
825
+ // Convert PCM to AudioBuffer
826
+ const pcmData = new Int16Array(arrayBuffer);
827
+ const floatData = new Float32Array(pcmData.length);
828
+
829
+ for (let i = 0; i < pcmData.length; i++) {
830
+ floatData[i] = pcmData[i] / 32768.0;
831
+ }
832
+
833
+ const audioBuffer = audioContext.createBuffer(1, floatData.length, 24000);
834
+ audioBuffer.getChannelData(0).set(floatData);
835
+
836
+ const source = audioContext.createBufferSource();
837
+ source.buffer = audioBuffer;
838
+ source.connect(audioContext.destination);
839
+
840
+ await new Promise((resolve) => {
841
+ source.onended = resolve;
842
+ source.start();
843
+ });
844
+ } catch (error) {
845
+ console.error('Audio playback error:', error);
846
+ }
847
+ }
848
+ }
849
+
850
+ function setMode(mode) {
851
+ currentMode = mode;
852
+
853
+ const buttons = document.querySelectorAll('.mode-toggle button');
854
+ buttons.forEach(btn => {
855
+ btn.classList.remove('active');
856
+ });
857
+ event.target.classList.add('active');
858
+
859
+ // Show/hide appropriate input areas
860
+ const textInputRow = document.getElementById('textInputRow');
861
+ const voiceInputRow = document.getElementById('voiceInputRow');
862
+
863
+ if (mode === 'text') {
864
+ textInputRow.style.display = 'flex';
865
+ voiceInputRow.style.display = 'none';
866
+ } else {
867
+ textInputRow.style.display = 'none';
868
+ voiceInputRow.style.display = 'flex';
869
+ }
870
+
871
+ if (session) {
872
+ addMessage('system', `⚠️ Mode changed to ${mode}. Please disconnect and reconnect to apply changes.`);
873
+ }
874
+ }
875
+
876
+ function addMessage(type, content) {
877
+ const messagesDiv = document.getElementById('chatMessages');
878
+ const messageDiv = document.createElement('div');
879
+ messageDiv.className = `message ${type}`;
880
+
881
+ const contentDiv = document.createElement('div');
882
+ contentDiv.className = 'message-content';
883
+ contentDiv.textContent = content;
884
+
885
+ messageDiv.appendChild(contentDiv);
886
+ messagesDiv.appendChild(messageDiv);
887
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
888
+ }
889
+
890
+ function showTypingIndicator() {
891
+ hideTypingIndicator();
892
+
893
+ const messagesDiv = document.getElementById('chatMessages');
894
+ const indicatorDiv = document.createElement('div');
895
+ indicatorDiv.className = 'message assistant';
896
+ indicatorDiv.id = 'typingIndicator';
897
+
898
+ const contentDiv = document.createElement('div');
899
+ contentDiv.className = 'message-content typing-indicator';
900
+ contentDiv.innerHTML = '<div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>';
901
+
902
+ indicatorDiv.appendChild(contentDiv);
903
+ messagesDiv.appendChild(indicatorDiv);
904
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
905
+ }
906
+
907
+ function hideTypingIndicator() {
908
+ const indicator = document.getElementById('typingIndicator');
909
+ if (indicator) {
910
+ indicator.remove();
911
+ }
912
+ }
913
+
914
+ function updateStatus(connected) {
915
+ const statusDot = document.getElementById('statusDot');
916
+ const statusText = document.getElementById('statusText');
917
+
918
+ if (connected) {
919
+ statusDot.classList.add('connected');
920
+ statusText.textContent = 'Connected';
921
+ } else {
922
+ statusDot.classList.remove('connected');
923
+ statusText.textContent = 'Disconnected';
924
+ }
925
+ }
926
+ </script>
927
+ </body>
928
+ </html>