gnumanth commited on
Commit
905d68e
Β·
1 Parent(s): 58eaa29

style: soft pastel theme and enhanced neural on-device TTS engine with voice selection

Browse files
Files changed (1) hide show
  1. index.html +343 -153
index.html CHANGED
@@ -6,152 +6,259 @@
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;
@@ -161,70 +268,89 @@
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;
@@ -235,68 +361,78 @@
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>
@@ -307,12 +443,12 @@
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>
@@ -326,11 +462,75 @@
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();
@@ -349,7 +549,7 @@
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");
@@ -359,7 +559,6 @@
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")) {
@@ -382,11 +581,9 @@
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) {
@@ -419,15 +616,6 @@
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);
@@ -449,6 +637,7 @@
449
 
450
  recognition.onstart = () => {
451
  isListening = true;
 
452
  orb.classList.add('listening');
453
  statusBadge.textContent = "πŸŽ™οΈ Listening to your voice...";
454
  };
@@ -467,7 +656,7 @@
467
  recognition.onerror = () => {
468
  isListening = false;
469
  orb.classList.remove('listening');
470
- statusBadge.textContent = "Tap orb to speak";
471
  };
472
  }
473
 
@@ -480,14 +669,15 @@
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>
 
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=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet">
10
  <style>
11
  :root {
12
+ /* Soft Pastel Color Palette */
13
+ --bg-gradient-1: #fbf9fe;
14
+ --bg-gradient-2: #f4f0fb;
15
+ --bg-gradient-3: #edf2fb;
16
+ --card-bg: rgba(255, 255, 255, 0.85);
17
+ --card-raised: #ffffff;
18
+ --border-pastel: #e2daf0;
19
+ --border-soft: #ede6f6;
20
+
21
+ --pastel-lavender: #8b5cf6;
22
+ --pastel-lavender-soft: #ede9fe;
23
+ --pastel-lavender-border: #ddd6fe;
24
+
25
+ --pastel-mint: #059669;
26
+ --pastel-mint-soft: #d1fae5;
27
+ --pastel-mint-border: #a7f3d0;
28
+
29
+ --pastel-peach: #ea580c;
30
+ --pastel-peach-soft: #ffedd5;
31
+ --pastel-peach-border: #fed7aa;
32
+
33
+ --pastel-rose: #e11d48;
34
+ --pastel-rose-soft: #ffe4e6;
35
+ --pastel-rose-border: #fecdd3;
36
+
37
+ --pastel-sky: #0284c7;
38
+ --pastel-sky-soft: #e0f2fe;
39
+ --pastel-sky-border: #bae6fd;
40
+
41
+ --text-main: #2e2640;
42
+ --text-muted: #6b6280;
43
+ --shadow-pastel: 0 12px 34px -8px rgba(139, 92, 246, 0.12), 0 4px 16px -2px rgba(0, 0, 0, 0.04);
44
+ --shadow-card: 0 20px 40px -12px rgba(112, 90, 160, 0.14), 0 2px 10px rgba(0, 0, 0, 0.03);
45
  }
46
+
47
  * { box-sizing: border-box; margin: 0; padding: 0; }
48
+
49
  body {
50
+ background: linear-gradient(135deg, var(--bg-gradient-1) 0%, var(--bg-gradient-2) 50%, var(--bg-gradient-3) 100%);
51
+ color: var(--text-main);
52
+ font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
53
  min-height: 100vh;
54
  display: flex;
55
  flex-direction: column;
56
  align-items: center;
57
+ padding: 28px 16px 48px;
58
  }
59
+
60
  header {
61
  text-align: center;
62
+ max-width: 860px;
63
+ margin-bottom: 24px;
64
  }
65
+
66
  .badge-row {
67
  display: flex;
68
  gap: 10px;
69
  justify-content: center;
70
+ margin-bottom: 14px;
71
  flex-wrap: wrap;
72
  }
73
+
74
  .badge {
75
+ background: var(--pastel-lavender-soft);
76
+ border: 1px solid var(--pastel-lavender-border);
77
+ color: #6d28d9;
78
+ padding: 5px 14px;
79
  border-radius: 9999px;
80
+ font-size: 0.8rem;
81
  font-family: 'JetBrains Mono', monospace;
82
  font-weight: 600;
83
  text-decoration: none;
84
+ transition: all 0.2s ease;
85
+ box-shadow: 0 2px 6px rgba(109, 40, 217, 0.06);
86
+ }
87
+ .badge:hover {
88
+ transform: translateY(-1px);
89
+ background: #e4dcfa;
90
+ }
91
+
92
+ .badge.mint {
93
+ background: var(--pastel-mint-soft);
94
+ border-color: var(--pastel-mint-border);
95
+ color: #047857;
96
+ }
97
+ .badge.mint:hover { background: #c2f5dc; }
98
+
99
+ .badge.peach {
100
+ background: var(--pastel-peach-soft);
101
+ border-color: var(--pastel-peach-border);
102
+ color: #c2410c;
103
  }
104
+ .badge.peach:hover { background: #fde0be; }
105
+
106
  h1 {
107
+ font-size: clamp(2rem, 4.5vw, 2.75rem);
108
+ font-weight: 800;
109
+ letter-spacing: -0.03em;
110
+ background: linear-gradient(135deg, #4c1d95 0%, #7c3aed 50%, #db2777 100%);
111
  -webkit-background-clip: text;
112
  -webkit-text-fill-color: transparent;
113
+ margin-bottom: 8px;
114
  }
115
+
116
  p.sub {
117
  color: var(--text-muted);
118
+ font-size: 1rem;
119
+ font-weight: 500;
120
  }
121
+
122
+ /* Layout Grid */
123
  .main-grid {
124
  display: grid;
125
  grid-template-columns: 1fr;
126
+ gap: 24px;
127
  width: 100%;
128
+ max-width: 1120px;
129
  }
130
+ @media (min-width: 880px) {
131
  .main-grid { grid-template-columns: 1.15fr 1fr; }
132
  }
133
+
134
  .panel {
135
  background: var(--card-bg);
136
+ backdrop-filter: blur(16px);
137
+ border: 1px solid var(--border-pastel);
138
+ border-radius: 24px;
139
+ padding: 24px;
140
  display: flex;
141
  flex-direction: column;
142
+ box-shadow: var(--shadow-card);
143
+ position: relative;
144
  }
145
+
146
+ /* Controls Bar */
147
+ .controls-bar {
148
+ display: flex;
149
+ justify-content: space-between;
150
+ align-items: center;
151
+ background: #ffffff;
152
+ border: 1px solid var(--border-soft);
153
+ padding: 8px 14px;
154
+ border-radius: 14px;
155
+ margin-bottom: 16px;
156
+ font-size: 0.82rem;
157
+ color: var(--text-muted);
158
+ flex-wrap: wrap;
159
+ gap: 8px;
160
+ }
161
+ .controls-bar select {
162
+ background: var(--pastel-lavender-soft);
163
+ border: 1px solid var(--pastel-lavender-border);
164
+ color: #5b21b6;
165
+ font-size: 0.8rem;
166
+ font-weight: 600;
167
+ padding: 4px 8px;
168
+ border-radius: 8px;
169
+ outline: none;
170
+ cursor: pointer;
171
+ }
172
+
173
  /* Voice Orb */
174
  .orb-container {
175
  display: flex;
176
  flex-direction: column;
177
  align-items: center;
178
  justify-content: center;
179
+ padding: 14px 0 10px;
180
  gap: 14px;
181
  }
182
+
183
  .voice-orb {
184
+ width: 110px;
185
+ height: 110px;
186
  border-radius: 50%;
187
+ background: radial-gradient(circle at 35% 35%, #c4b5fd 0%, #a78bfa 40%, #7c3aed 100%);
188
+ box-shadow: 0 10px 30px rgba(124, 58, 237, 0.35), 0 0 0 6px rgba(167, 139, 250, 0.25);
189
  display: flex;
190
  align-items: center;
191
  justify-content: center;
192
  cursor: pointer;
193
+ transition: all 0.35s cubic-bezier(0.16, 1, 0.3, 1);
194
  position: relative;
195
  }
196
+ .voice-orb:hover {
197
+ transform: scale(1.06);
198
+ box-shadow: 0 14px 38px rgba(124, 58, 237, 0.45), 0 0 0 8px rgba(167, 139, 250, 0.35);
199
+ }
200
  .voice-orb.listening {
201
+ transform: scale(1.14);
202
+ background: radial-gradient(circle at 35% 35%, #6ee7b7 0%, #34d399 40%, #059669 100%);
203
+ box-shadow: 0 14px 45px rgba(16, 185, 129, 0.5), 0 0 0 10px rgba(52, 211, 153, 0.35);
204
+ animation: pastelPulse 1.5s infinite alternate ease-in-out;
205
+ }
206
+ .voice-orb.speaking {
207
+ background: radial-gradient(circle at 35% 35%, #fbcfe8 0%, #f472b6 40%, #db2777 100%);
208
+ box-shadow: 0 14px 45px rgba(219, 39, 119, 0.45), 0 0 0 10px rgba(244, 114, 182, 0.35);
209
+ animation: speakingPulse 1.2s infinite alternate ease-in-out;
210
+ }
211
+
212
+ @keyframes pastelPulse {
213
+ 0% { transform: scale(1.08); box-shadow: 0 10px 30px rgba(16, 185, 129, 0.4), 0 0 0 6px rgba(52, 211, 153, 0.3); }
214
+ 100% { transform: scale(1.18); box-shadow: 0 18px 50px rgba(16, 185, 129, 0.6), 0 0 0 12px rgba(52, 211, 153, 0.4); }
215
  }
216
+ @keyframes speakingPulse {
217
+ 0% { transform: scale(1.05); box-shadow: 0 10px 30px rgba(219, 39, 119, 0.35), 0 0 0 6px rgba(244, 114, 182, 0.25); }
218
+ 100% { transform: scale(1.12); box-shadow: 0 16px 45px rgba(219, 39, 119, 0.55), 0 0 0 12px rgba(244, 114, 182, 0.4); }
219
  }
220
+
221
+ .mic-icon { width: 44px; height: 44px; fill: white; pointer-events: none; }
222
+
223
  .status-badge {
224
+ font-size: 0.84rem;
225
+ color: #5b21b6;
226
  font-family: 'JetBrains Mono', monospace;
227
+ font-weight: 600;
228
+ background: #ffffff;
229
+ border: 1px solid var(--border-soft);
230
+ padding: 5px 14px;
231
  border-radius: 9999px;
232
+ box-shadow: 0 2px 8px rgba(0,0,0,0.03);
233
  }
234
+
235
+ /* Pastel Action Chips */
236
  .chips-row {
237
  display: flex;
238
  gap: 8px;
239
  flex-wrap: wrap;
240
+ margin: 12px 0 14px;
241
  }
242
  .chip {
243
+ background: #ffffff;
244
+ border: 1px solid var(--border-pastel);
245
+ color: var(--text-main);
246
+ font-size: 0.8rem;
247
+ font-weight: 600;
248
+ padding: 6px 13px;
249
  border-radius: 9999px;
250
  cursor: pointer;
251
+ transition: all 0.2s ease;
252
+ box-shadow: 0 2px 5px rgba(0,0,0,0.02);
253
  }
254
  .chip:hover {
255
+ background: var(--pastel-lavender-soft);
256
+ border-color: var(--pastel-lavender-border);
257
+ color: #6d28d9;
258
+ transform: translateY(-1px);
259
  }
260
+
261
+ /* Chat Messages Box */
262
  .chat-box {
263
  flex: 1;
264
  min-height: 240px;
 
268
  flex-direction: column;
269
  gap: 12px;
270
  padding-right: 6px;
271
+ margin: 8px 0 16px;
272
  }
273
+ .chat-box::-webkit-scrollbar { width: 5px; }
274
+ .chat-box::-webkit-scrollbar-thumb { background: #ddd6fe; border-radius: 4px; }
275
+
276
  .msg {
277
+ padding: 12px 16px;
278
+ border-radius: 16px;
279
+ font-size: 0.92rem;
280
+ line-height: 1.5;
281
+ max-width: 88%;
282
+ box-shadow: 0 2px 8px rgba(0,0,0,0.03);
283
  }
284
  .msg.user {
285
  align-self: flex-end;
286
+ background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
287
+ color: #ffffff;
288
+ border-bottom-right-radius: 4px;
289
  }
290
  .msg.agent {
291
  align-self: flex-start;
292
+ background: #ffffff;
293
+ border: 1px solid var(--border-soft);
294
+ color: var(--text-main);
295
+ border-bottom-left-radius: 4px;
296
  }
297
+
298
  .tool-call-pill {
299
  display: inline-flex;
300
  align-items: center;
301
+ gap: 6px;
302
+ font-size: 0.74rem;
303
  font-family: 'JetBrains Mono', monospace;
304
+ font-weight: 600;
305
+ color: #b45309;
306
+ background: #fef3c7;
307
+ border: 1px solid #fde68a;
308
+ padding: 3px 10px;
309
+ border-radius: 6px;
310
+ margin-bottom: 8px;
311
  }
312
+
313
+ /* Input area */
314
  .input-row {
315
  display: flex;
316
+ gap: 10px;
317
  margin-top: auto;
318
  }
319
  input[type="text"] {
320
  flex: 1;
321
+ background: #ffffff;
322
+ border: 1px solid var(--border-pastel);
323
+ color: var(--text-main);
324
+ padding: 12px 16px;
325
+ border-radius: 14px;
326
  outline: none;
327
  font-family: inherit;
328
+ font-size: 0.92rem;
329
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
330
+ transition: border-color 0.2s ease;
331
+ }
332
+ input[type="text"]:focus {
333
+ border-color: var(--pastel-lavender);
334
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
335
  }
 
336
  button.btn-send {
337
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
338
  color: white;
339
  border: none;
340
+ padding: 0 22px;
341
+ border-radius: 14px;
342
+ font-weight: 700;
343
+ font-size: 0.92rem;
344
  cursor: pointer;
345
+ transition: all 0.2s ease;
346
+ box-shadow: 0 4px 12px rgba(124, 58, 237, 0.25);
347
+ }
348
+ button.btn-send:hover {
349
+ transform: translateY(-1px);
350
+ box-shadow: 0 6px 18px rgba(124, 58, 237, 0.35);
351
  }
352
+
353
+ /* Right Comic Display Card */
354
  .comic-card {
355
  display: flex;
356
  flex-direction: column;
 
361
  }
362
  .comic-placeholder {
363
  color: var(--text-muted);
364
+ font-size: 0.95rem;
365
+ padding: 48px 20px;
366
  }
367
  .comic-title {
368
+ font-size: 1.35rem;
369
+ font-weight: 800;
370
+ color: #3b0764;
371
+ margin-bottom: 10px;
372
  }
373
  .comic-img-wrap {
374
  background: #ffffff;
375
+ padding: 16px;
376
+ border-radius: 16px;
377
  margin: 12px 0;
378
  max-width: 100%;
379
+ border: 1px solid var(--border-soft);
380
+ box-shadow: 0 10px 30px rgba(124, 58, 237, 0.08);
381
  }
382
  .comic-img {
383
  max-width: 100%;
384
+ max-height: 290px;
385
  object-fit: contain;
386
  display: block;
387
+ border-radius: 6px;
388
  }
389
  .comic-alt {
390
+ font-size: 0.86rem;
391
+ color: #4c1d95;
392
+ background: var(--pastel-lavender-soft);
393
+ border-left: 4px solid var(--pastel-lavender);
394
+ padding: 12px 16px;
395
+ border-radius: 0 12px 12px 0;
396
  text-align: left;
397
+ margin-top: 10px;
398
+ line-height: 1.5;
399
+ font-weight: 500;
400
  }
401
  </style>
402
  </head>
403
  <body>
404
  <header>
405
  <div class="badge-row">
406
+ <a class="badge" href="https://huggingface.co/pipecat-ai/phonellm-alpha-1" target="_blank">πŸŽ™οΈ PhoneLLM Alpha 1</a>
407
+ <span class="badge mint">⚑ Pipecat Voice Engine</span>
408
+ <a class="badge peach" href="https://xkcd.hemanth.deno.net/" target="_blank">🌐 xkcd.hemanth.deno.net</a>
409
  </div>
410
  <h1>PhoneLLM XKCD Voice Agent</h1>
411
+ <p class="sub">Soft pastel voice interface with high-fidelity on-device neural speech & live XKCD tool calls.</p>
412
  </header>
413
 
414
  <main class="main-grid">
415
+ <!-- Left Voice & Conversation Panel -->
416
  <div class="panel">
417
+ <div class="controls-bar">
418
+ <span>πŸ”Š <strong>On-Device Voice:</strong></span>
419
+ <select id="voice-select">
420
+ <option value="default">Loading high-quality neural voices...</option>
421
+ </select>
422
+ </div>
423
+
424
  <div class="orb-container">
425
  <div id="voice-orb" class="voice-orb" title="Click to Speak / Stop">
426
  <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>
427
  </div>
428
+ <span id="status-badge" class="status-badge">Tap mic orb or select a prompt below</span>
429
  </div>
430
 
431
  <div class="chips-row">
432
+ <button class="chip" onclick="sendQuery('Show me the latest XKCD comic')">✨ Latest Comic</button>
433
+ <button class="chip" onclick="sendQuery('Give me a random XKCD comic and explain it')">🎲 Random Comic</button>
434
+ <button class="chip" onclick="sendQuery('What is XKCD comic 327 (Little Bobby Tables)?')">πŸ’» Little Bobby Tables (#327)</button>
435
+ <button class="chip" onclick="sendQuery('Show comic 300')">πŸ“± Comic #300</button>
436
  </div>
437
 
438
  <div id="chat-box" class="chat-box"></div>
 
443
  </div>
444
  </div>
445
 
446
+ <!-- Right Live XKCD Display Panel -->
447
  <div class="panel">
448
  <div id="comic-card" class="comic-card">
449
  <div class="comic-placeholder">
450
+ <svg width="54" height="54" fill="none" stroke="#a78bfa" stroke-width="1.5" viewBox="0 0 24 24" style="margin-bottom:12px;"><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>
451
+ <p>Ask the agent for an XKCD comic to render it live here.</p>
452
  </div>
453
  </div>
454
  </div>
 
462
  const userInput = document.getElementById('user-input');
463
  const sendBtn = document.getElementById('send-btn');
464
  const comicCard = document.getElementById('comic-card');
465
+ const voiceSelect = document.getElementById('voice-select');
466
 
467
  let isListening = false;
468
  let recognition = null;
469
+ let selectedVoice = null;
470
+ let availableVoices = [];
471
+
472
+ // Initialize High-Quality On-Device TTS Voices
473
+ function loadVoices() {
474
+ if (!('speechSynthesis' in window)) return;
475
+ availableVoices = window.speechSynthesis.getVoices();
476
+ if (!availableVoices.length) return;
477
+
478
+ voiceSelect.innerHTML = '';
479
+
480
+ // Filter for premium / natural English voices (Samantha, Daniel, Siri, Google US, Natural, etc.)
481
+ const preferred = availableVoices.filter(v =>
482
+ v.lang.startsWith('en') &&
483
+ (v.name.includes('Natural') || v.name.includes('Neural') || v.name.includes('Samantha') || v.name.includes('Google') || v.name.includes('Siri') || v.name.includes('Daniel') || v.name.includes('Karen') || v.name.includes('Alex'))
484
+ );
485
+
486
+ const voicesToShow = preferred.length > 0 ? preferred : availableVoices.filter(v => v.lang.startsWith('en'));
487
 
488
+ voicesToShow.forEach((v, idx) => {
489
+ const opt = document.createElement('option');
490
+ opt.value = idx;
491
+ opt.textContent = `${v.name} (${v.lang})`;
492
+ voiceSelect.appendChild(opt);
493
+ });
494
+
495
+ // Default to the highest quality natural neural voice
496
+ selectedVoice = voicesToShow[0] || availableVoices[0];
497
+ voiceSelect.onchange = () => {
498
+ selectedVoice = voicesToShow[voiceSelect.value] || availableVoices[0];
499
+ };
500
+ }
501
+
502
+ if ('speechSynthesis' in window) {
503
+ speechSynthesis.onvoiceschanged = loadVoices;
504
+ loadVoices();
505
+ }
506
+
507
+ // High-Fidelity Speech Synthesis
508
+ function speak(text) {
509
+ if (!('speechSynthesis' in window)) return;
510
+ window.speechSynthesis.cancel();
511
+
512
+ orb.classList.remove('listening');
513
+ orb.classList.add('speaking');
514
+ statusBadge.textContent = "πŸ”Š Speaking response...";
515
+
516
+ const utterance = new SpeechSynthesisUtterance(text);
517
+ if (selectedVoice) utterance.voice = selectedVoice;
518
+ utterance.rate = 1.02;
519
+ utterance.pitch = 1.0;
520
+
521
+ utterance.onend = () => {
522
+ orb.classList.remove('speaking');
523
+ statusBadge.textContent = "Ready";
524
+ };
525
+ utterance.onerror = () => {
526
+ orb.classList.remove('speaking');
527
+ statusBadge.textContent = "Ready";
528
+ };
529
+
530
+ window.speechSynthesis.speak(utterance);
531
+ }
532
+
533
+ // Live XKCD API Tool Functions
534
  async function toolGetLatestXkcd() {
535
  const res = await fetch(`${XKCD_API_BASE}/`);
536
  const json = await res.json();
 
549
  return json.data || json;
550
  }
551
 
552
+ // PhoneLLM Conversational & Tool Calling Engine
553
  async function processPhoneLLMTurn(userText) {
554
  statusBadge.textContent = "⚑ PhoneLLM Reasoning...";
555
  addMessage(userText, "user");
 
559
  let toolName = null;
560
  let replyText = "";
561
 
 
562
  const numMatch = lower.match(/\b(?:comic|number|#)?\s*(\d+)\b/);
563
 
564
  if (lower.includes("random") || lower.includes("any comic")) {
 
581
  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!`;
582
  }
583
 
 
584
  addMessage(replyText, "agent", toolName);
585
  if (comic) renderComic(comic);
586
  speak(replyText);
 
587
  }
588
 
589
  function addMessage(text, sender, toolName = null) {
 
616
  `;
617
  }
618
 
 
 
 
 
 
 
 
 
 
619
  window.sendQuery = function(text) {
620
  if (!text) return;
621
  processPhoneLLMTurn(text);
 
637
 
638
  recognition.onstart = () => {
639
  isListening = true;
640
+ orb.classList.remove('speaking');
641
  orb.classList.add('listening');
642
  statusBadge.textContent = "πŸŽ™οΈ Listening to your voice...";
643
  };
 
656
  recognition.onerror = () => {
657
  isListening = false;
658
  orb.classList.remove('listening');
659
+ statusBadge.textContent = "Tap mic orb to speak";
660
  };
661
  }
662
 
 
669
  recognition.stop();
670
  } else {
671
  window.speechSynthesis.cancel();
672
+ orb.classList.remove('speaking');
673
  recognition.start();
674
  }
675
  });
676
 
677
+ // Initial greeting
678
  setTimeout(() => {
679
+ addMessage("Hello! I am your Pipecat voice assistant powered by PhoneLLM Alpha 1. Tap the glowing pastel orb or select a prompt below to look up XKCD comics!", "agent");
680
+ }, 400);
681
  </script>
682
  </body>
683
  </html>