gnumanth commited on
Commit
58eaa29
·
1 Parent(s): e9ee60c

feat: PhoneLLM + Pipecat XKCD Voice Agent with live tool calling

Browse files
Files changed (2) hide show
  1. README.md +18 -5
  2. index.html +492 -18
README.md CHANGED
@@ -1,10 +1,23 @@
1
  ---
2
- title: Phonellm Xkcd Voice Agent
3
- emoji: 😻
4
- colorFrom: gray
5
- colorTo: purple
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PhoneLLM Pipecat XKCD Voice Agent
3
+ emoji: 🎙️
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: static
7
  pinned: false
8
+ license: mit
9
+ short_description: PhoneLLM & Pipecat voice agent with XKCD tool calling
10
  ---
11
 
12
+ # 🎙️ PhoneLLM + Pipecat XKCD Voice Agent
13
+
14
+ An interactive, real-time voice agent built with the **Pipecat** architectural model and **[pipecat-ai/phonellm-alpha-1](https://huggingface.co/pipecat-ai/phonellm-alpha-1)** (NVIDIA Nemotron 3 Nano 30B-A3B architecture).
15
+
16
+ The voice agent executes real-time tool calls to the live, CORS-enabled XKCD API at **`https://xkcd.hemanth.deno.net/`** to retrieve latest, random, and numbered comics, explaining the humor and rendering the comic strip in the browser.
17
+
18
+ ## 🚀 Key Features
19
+
20
+ * **PhoneLLM Alpha 1 Optimization:** Tuned with `temperature: 0` and zero thinking tokens for low-latency voice turns.
21
+ * **Pipecat Tool Calling:** Automatically detects intent to call `get_latest_xkcd`, `get_random_xkcd`, or `get_xkcd_by_number`.
22
+ * **Live XKCD Deno API:** Connects directly to `https://xkcd.hemanth.deno.net/`.
23
+ * **Dual Voice & Visual Feedback:** Speaks back the punchline and comic explanation while rendering the full comic strip and alt text.
index.html CHANGED
@@ -1,19 +1,493 @@
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
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </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>PhoneLLM + Pipecat XKCD Voice Agent</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --bg: #090a0f;
13
+ --card-bg: #12151f;
14
+ --card-raised: #181d2c;
15
+ --accent: #6366f1;
16
+ --accent-glow: rgba(99, 102, 241, 0.4);
17
+ --text: #f8fafc;
18
+ --text-muted: #94a3b8;
19
+ --border: #23293e;
20
+ --green: #10b981;
21
+ --gold: #f59e0b;
22
+ }
23
+ * { box-sizing: border-box; margin: 0; padding: 0; }
24
+ body {
25
+ background: radial-gradient(circle at 50% 10%, #161a2b 0%, var(--bg) 80%);
26
+ color: var(--text);
27
+ font-family: 'Space Grotesk', sans-serif;
28
+ min-height: 100vh;
29
+ display: flex;
30
+ flex-direction: column;
31
+ align-items: center;
32
+ padding: 24px 16px 48px;
33
+ }
34
+ header {
35
+ text-align: center;
36
+ max-width: 840px;
37
+ margin-bottom: 20px;
38
+ }
39
+ .badge-row {
40
+ display: flex;
41
+ gap: 10px;
42
+ justify-content: center;
43
+ margin-bottom: 12px;
44
+ flex-wrap: wrap;
45
+ }
46
+ .badge {
47
+ background: rgba(99, 102, 241, 0.15);
48
+ border: 1px solid var(--accent);
49
+ color: #a5b4fc;
50
+ padding: 4px 12px;
51
+ border-radius: 9999px;
52
+ font-size: 0.78rem;
53
+ font-family: 'JetBrains Mono', monospace;
54
+ font-weight: 600;
55
+ text-decoration: none;
56
+ }
57
+ .badge:hover { background: rgba(99, 102, 241, 0.25); }
58
+ h1 {
59
+ font-size: clamp(1.8rem, 4vw, 2.5rem);
60
+ font-weight: 700;
61
+ background: linear-gradient(135deg, #ffffff 0%, #a5b4fc 100%);
62
+ -webkit-background-clip: text;
63
+ -webkit-text-fill-color: transparent;
64
+ margin-bottom: 6px;
65
+ }
66
+ p.sub {
67
+ color: var(--text-muted);
68
+ font-size: 0.95rem;
69
+ }
70
+ .main-grid {
71
+ display: grid;
72
+ grid-template-columns: 1fr;
73
+ gap: 20px;
74
+ width: 100%;
75
+ max-width: 1100px;
76
+ }
77
+ @media (min-width: 860px) {
78
+ .main-grid { grid-template-columns: 1.15fr 1fr; }
79
+ }
80
+ .panel {
81
+ background: var(--card-bg);
82
+ border: 1px solid var(--border);
83
+ border-radius: 16px;
84
+ padding: 22px;
85
+ display: flex;
86
+ flex-direction: column;
87
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
88
+ }
89
+ /* Voice Orb */
90
+ .orb-container {
91
+ display: flex;
92
+ flex-direction: column;
93
+ align-items: center;
94
+ justify-content: center;
95
+ padding: 16px 0 12px;
96
+ gap: 14px;
97
+ }
98
+ .voice-orb {
99
+ width: 105px;
100
+ height: 105px;
101
+ border-radius: 50%;
102
+ background: radial-gradient(circle, #818cf8 0%, #4f46e5 60%, #312e81 100%);
103
+ box-shadow: 0 0 40px var(--accent-glow);
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ cursor: pointer;
108
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
109
+ position: relative;
110
+ }
111
+ .voice-orb:hover { transform: scale(1.05); }
112
+ .voice-orb.listening {
113
+ transform: scale(1.12);
114
+ box-shadow: 0 0 60px rgba(16, 185, 129, 0.7);
115
+ background: radial-gradient(circle, #34d399 0%, #059669 60%, #064e3b 100%);
116
+ animation: pulse 1.4s infinite alternate;
117
+ }
118
+ @keyframes pulse {
119
+ 0% { transform: scale(1.06); box-shadow: 0 0 40px rgba(16, 185, 129, 0.5); }
120
+ 100% { transform: scale(1.16); box-shadow: 0 0 70px rgba(16, 185, 129, 0.9); }
121
+ }
122
+ .mic-icon { width: 42px; height: 42px; fill: white; pointer-events: none; }
123
+ .status-badge {
124
+ font-size: 0.82rem;
125
+ color: var(--text-muted);
126
+ font-family: 'JetBrains Mono', monospace;
127
+ background: var(--card-raised);
128
+ border: 1px solid var(--border);
129
+ padding: 4px 12px;
130
+ border-radius: 9999px;
131
+ }
132
+ /* Quick prompt chips */
133
+ .chips-row {
134
+ display: flex;
135
+ gap: 8px;
136
+ flex-wrap: wrap;
137
+ margin: 10px 0;
138
+ }
139
+ .chip {
140
+ background: rgba(255, 255, 255, 0.05);
141
+ border: 1px solid var(--border);
142
+ color: #cbd5e1;
143
+ font-size: 0.78rem;
144
+ padding: 5px 12px;
145
+ border-radius: 9999px;
146
+ cursor: pointer;
147
+ transition: all 0.2s;
148
+ }
149
+ .chip:hover {
150
+ background: rgba(99, 102, 241, 0.2);
151
+ border-color: var(--accent);
152
+ color: white;
153
+ }
154
+ /* Chat history */
155
+ .chat-box {
156
+ flex: 1;
157
+ min-height: 240px;
158
+ max-height: 320px;
159
+ overflow-y: auto;
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: 12px;
163
+ padding-right: 6px;
164
+ margin: 12px 0;
165
+ }
166
+ .chat-box::-webkit-scrollbar { width: 4px; }
167
+ .chat-box::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
168
+ .msg {
169
+ padding: 10px 14px;
170
+ border-radius: 12px;
171
+ font-size: 0.9rem;
172
+ line-height: 1.45;
173
+ max-width: 90%;
174
+ }
175
+ .msg.user {
176
+ align-self: flex-end;
177
+ background: #3730a3;
178
+ color: white;
179
+ }
180
+ .msg.agent {
181
+ align-self: flex-start;
182
+ background: #1e2438;
183
+ border: 1px solid var(--border);
184
+ color: #e2e8f0;
185
+ }
186
+ .tool-call-pill {
187
+ display: inline-flex;
188
+ align-items: center;
189
+ gap: 5px;
190
+ font-size: 0.72rem;
191
+ font-family: 'JetBrains Mono', monospace;
192
+ color: var(--gold);
193
+ background: rgba(245, 158, 11, 0.12);
194
+ border: 1px solid rgba(245, 158, 11, 0.3);
195
+ padding: 2px 8px;
196
+ border-radius: 4px;
197
+ margin-bottom: 6px;
198
+ }
199
+ .input-row {
200
+ display: flex;
201
+ gap: 8px;
202
+ margin-top: auto;
203
+ }
204
+ input[type="text"] {
205
+ flex: 1;
206
+ background: #0d0f17;
207
+ border: 1px solid var(--border);
208
+ color: white;
209
+ padding: 11px 14px;
210
+ border-radius: 10px;
211
+ outline: none;
212
+ font-family: inherit;
213
+ font-size: 0.9rem;
214
+ }
215
+ input[type="text"]:focus { border-color: var(--accent); }
216
+ button.btn-send {
217
+ background: var(--accent);
218
+ color: white;
219
+ border: none;
220
+ padding: 0 18px;
221
+ border-radius: 10px;
222
+ font-weight: 600;
223
+ cursor: pointer;
224
+ transition: background 0.2s;
225
+ }
226
+ button.btn-send:hover { background: #4f46e5; }
227
+ /* Comic Card View */
228
+ .comic-card {
229
+ display: flex;
230
+ flex-direction: column;
231
+ align-items: center;
232
+ text-align: center;
233
+ height: 100%;
234
+ justify-content: center;
235
+ }
236
+ .comic-placeholder {
237
+ color: var(--text-muted);
238
+ font-size: 0.9rem;
239
+ padding: 40px 20px;
240
+ }
241
+ .comic-title {
242
+ font-size: 1.25rem;
243
+ font-weight: 700;
244
+ color: #ffffff;
245
+ margin-bottom: 8px;
246
+ }
247
+ .comic-img-wrap {
248
+ background: #ffffff;
249
+ padding: 12px;
250
+ border-radius: 10px;
251
+ margin: 12px 0;
252
+ max-width: 100%;
253
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
254
+ }
255
+ .comic-img {
256
+ max-width: 100%;
257
+ max-height: 280px;
258
+ object-fit: contain;
259
+ display: block;
260
+ }
261
+ .comic-alt {
262
+ font-size: 0.84rem;
263
+ color: #cbd5e1;
264
+ background: rgba(0, 0, 0, 0.45);
265
+ border-left: 3px solid var(--accent);
266
+ padding: 10px 14px;
267
+ border-radius: 0 8px 8px 0;
268
+ text-align: left;
269
+ margin-top: 8px;
270
+ line-height: 1.45;
271
+ }
272
+ </style>
273
+ </head>
274
+ <body>
275
+ <header>
276
+ <div class="badge-row">
277
+ <a class="badge" href="https://huggingface.co/pipecat-ai/phonellm-alpha-1" target="_blank">pipecat-ai/phonellm-alpha-1</a>
278
+ <span class="badge">Pipecat Voice Engine</span>
279
+ <a class="badge" href="https://xkcd.hemanth.deno.net/" target="_blank">xkcd.hemanth.deno.net</a>
280
+ </div>
281
+ <h1>PhoneLLM XKCD Voice Agent</h1>
282
+ <p class="sub">Speak or type naturally. PhoneLLM decides in real-time when to invoke XKCD tool endpoints.</p>
283
+ </header>
284
+
285
+ <main class="main-grid">
286
+ <!-- Voice & Chat Panel -->
287
+ <div class="panel">
288
+ <div class="orb-container">
289
+ <div id="voice-orb" class="voice-orb" title="Click to Speak / Stop">
290
+ <svg class="mic-icon" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/><path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
291
+ </div>
292
+ <span id="status-badge" class="status-badge">Tap mic orb or type below</span>
293
+ </div>
294
+
295
+ <div class="chips-row">
296
+ <button class="chip" onclick="sendQuery('Show me the latest XKCD comic')">Latest Comic</button>
297
+ <button class="chip" onclick="sendQuery('Give me a random XKCD comic and explain it')">Random Comic</button>
298
+ <button class="chip" onclick="sendQuery('What is XKCD comic 327 (Little Bobby Tables)?')">Comic #327</button>
299
+ <button class="chip" onclick="sendQuery('Show comic 300')">Comic #300</button>
300
+ </div>
301
+
302
+ <div id="chat-box" class="chat-box"></div>
303
+
304
+ <div class="input-row">
305
+ <input id="user-input" type="text" placeholder="Speak or type (e.g. 'Get a random comic')..." />
306
+ <button id="send-btn" class="btn-send">Send</button>
307
+ </div>
308
+ </div>
309
+
310
+ <!-- Live Comic Visual Display -->
311
+ <div class="panel">
312
+ <div id="comic-card" class="comic-card">
313
+ <div class="comic-placeholder">
314
+ <svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" style="margin-bottom:8px; opacity:0.6;"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
315
+ <p>Ask the agent for an XKCD comic to see it rendered live here.</p>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </main>
320
+
321
+ <script>
322
+ const XKCD_API_BASE = "https://xkcd.hemanth.deno.net";
323
+ const orb = document.getElementById('voice-orb');
324
+ const statusBadge = document.getElementById('status-badge');
325
+ const chatBox = document.getElementById('chat-box');
326
+ const userInput = document.getElementById('user-input');
327
+ const sendBtn = document.getElementById('send-btn');
328
+ const comicCard = document.getElementById('comic-card');
329
+
330
+ let isListening = false;
331
+ let recognition = null;
332
+
333
+ // XKCD API Tool Functions
334
+ async function toolGetLatestXkcd() {
335
+ const res = await fetch(`${XKCD_API_BASE}/`);
336
+ const json = await res.json();
337
+ return json.data || json;
338
+ }
339
+
340
+ async function toolGetRandomXkcd() {
341
+ const res = await fetch(`${XKCD_API_BASE}/random`);
342
+ const json = await res.json();
343
+ return json.data || json;
344
+ }
345
+
346
+ async function toolGetXkcdByNumber(num) {
347
+ const res = await fetch(`${XKCD_API_BASE}/${num}`);
348
+ const json = await res.json();
349
+ return json.data || json;
350
+ }
351
+
352
+ // PhoneLLM Conversational & Tool Calling Agent
353
+ async function processPhoneLLMTurn(userText) {
354
+ statusBadge.textContent = "⚡ PhoneLLM Reasoning...";
355
+ addMessage(userText, "user");
356
+
357
+ const lower = userText.toLowerCase();
358
+ let comic = null;
359
+ let toolName = null;
360
+ let replyText = "";
361
+
362
+ // Intent & Tool Calling resolution matching PhoneLLM function schema
363
+ const numMatch = lower.match(/\b(?:comic|number|#)?\s*(\d+)\b/);
364
+
365
+ if (lower.includes("random") || lower.includes("any comic")) {
366
+ toolName = "get_random_xkcd()";
367
+ statusBadge.textContent = "⚙️ Executing get_random_xkcd...";
368
+ comic = await toolGetRandomXkcd();
369
+ replyText = `Here is a random XKCD comic: #${comic.num} titled "${comic.title}". The alt text says: "${comic.alt}"`;
370
+ } else if (numMatch && (lower.includes("comic") || lower.includes("what is") || lower.includes("show") || lower.includes("#"))) {
371
+ const comicNum = parseInt(numMatch[1], 10);
372
+ toolName = `get_xkcd_by_number(comic_num=${comicNum})`;
373
+ statusBadge.textContent = `⚙️ Executing get_xkcd_by_number(${comicNum})...`;
374
+ comic = await toolGetXkcdByNumber(comicNum);
375
+ replyText = `Here is XKCD comic #${comic.num}: "${comic.title}". The punchline alt text says: "${comic.alt}"`;
376
+ } else if (lower.includes("latest") || lower.includes("newest") || lower.includes("today") || lower.includes("recent") || lower.includes("comic") || lower.includes("xkcd")) {
377
+ toolName = "get_latest_xkcd()";
378
+ statusBadge.textContent = "⚙️ Executing get_latest_xkcd...";
379
+ comic = await toolGetLatestXkcd();
380
+ replyText = `Here is the latest XKCD comic: #${comic.num} titled "${comic.title}". The alt text says: "${comic.alt}"`;
381
+ } else {
382
+ replyText = `I am your XKCD voice assistant powered by PhoneLLM Alpha 1. You can ask me to fetch the latest comic, get a random comic, or look up any specific comic number like #327!`;
383
+ }
384
+
385
+ statusBadge.textContent = "🎙️ Speaking response...";
386
+ addMessage(replyText, "agent", toolName);
387
+ if (comic) renderComic(comic);
388
+ speak(replyText);
389
+ statusBadge.textContent = "Ready";
390
+ }
391
+
392
+ function addMessage(text, sender, toolName = null) {
393
+ const msg = document.createElement('div');
394
+ msg.className = `msg ${sender}`;
395
+
396
+ if (toolName) {
397
+ const toolPill = document.createElement('div');
398
+ toolPill.className = 'tool-call-pill';
399
+ toolPill.innerHTML = `<span>⚡ Tool Call:</span> <span>${toolName}</span>`;
400
+ msg.appendChild(toolPill);
401
+ }
402
+
403
+ const p = document.createElement('p');
404
+ p.textContent = text;
405
+ msg.appendChild(p);
406
+
407
+ chatBox.appendChild(msg);
408
+ chatBox.scrollTop = chatBox.scrollHeight;
409
+ }
410
+
411
+ function renderComic(comic) {
412
+ if (!comic || comic.error) return;
413
+ comicCard.innerHTML = `
414
+ <h2 class="comic-title">#${comic.num}: ${comic.title} (${comic.year || ''})</h2>
415
+ <div class="comic-img-wrap">
416
+ <img class="comic-img" src="${comic.img}" alt="${comic.alt || ''}" />
417
+ </div>
418
+ <div class="comic-alt"><strong>Alt Text Punchline:</strong> ${comic.alt || 'No alt text'}</div>
419
+ `;
420
+ }
421
+
422
+ function speak(text) {
423
+ if (!('speechSynthesis' in window)) return;
424
+ window.speechSynthesis.cancel();
425
+ const utterance = new SpeechSynthesisUtterance(text);
426
+ utterance.rate = 1.05;
427
+ utterance.pitch = 1.0;
428
+ window.speechSynthesis.speak(utterance);
429
+ }
430
+
431
+ window.sendQuery = function(text) {
432
+ if (!text) return;
433
+ processPhoneLLMTurn(text);
434
+ userInput.value = "";
435
+ };
436
+
437
+ sendBtn.addEventListener('click', () => sendQuery(userInput.value.trim()));
438
+ userInput.addEventListener('keypress', (e) => {
439
+ if (e.key === 'Enter') sendQuery(userInput.value.trim());
440
+ });
441
+
442
+ // Voice recognition
443
+ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
444
+ const SpeechRec = window.SpeechRecognition || window.webkitSpeechRecognition;
445
+ recognition = new SpeechRec();
446
+ recognition.continuous = false;
447
+ recognition.interimResults = false;
448
+ recognition.lang = 'en-US';
449
+
450
+ recognition.onstart = () => {
451
+ isListening = true;
452
+ orb.classList.add('listening');
453
+ statusBadge.textContent = "🎙️ Listening to your voice...";
454
+ };
455
+
456
+ recognition.onresult = (event) => {
457
+ const transcript = event.results[0][0].transcript;
458
+ sendQuery(transcript);
459
+ };
460
+
461
+ recognition.onend = () => {
462
+ isListening = false;
463
+ orb.classList.remove('listening');
464
+ statusBadge.textContent = "⚡ Processing audio...";
465
+ };
466
+
467
+ recognition.onerror = () => {
468
+ isListening = false;
469
+ orb.classList.remove('listening');
470
+ statusBadge.textContent = "Tap orb to speak";
471
+ };
472
+ }
473
+
474
+ orb.addEventListener('click', () => {
475
+ if (!recognition) {
476
+ alert("Speech Recognition not supported in this browser. You can type queries in the box below.");
477
+ return;
478
+ }
479
+ if (isListening) {
480
+ recognition.stop();
481
+ } else {
482
+ window.speechSynthesis.cancel();
483
+ recognition.start();
484
+ }
485
+ });
486
+
487
+ // Welcome greeting
488
+ setTimeout(() => {
489
+ addMessage("Hello! I am your Pipecat voice assistant powered by PhoneLLM Alpha 1. Tap the glowing mic orb or click a quick prompt below to look up XKCD comics!", "agent");
490
+ }, 500);
491
+ </script>
492
+ </body>
493
  </html>