fix: add search_xkcd_by_topic tool for topics like Python (#353), Git (#1597), and SQL (#327)
Browse files- index.html +72 -11
index.html
CHANGED
|
@@ -408,7 +408,7 @@
|
|
| 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
|
| 412 |
</header>
|
| 413 |
|
| 414 |
<main class="main-grid">
|
|
@@ -429,16 +429,17 @@
|
|
| 429 |
</div>
|
| 430 |
|
| 431 |
<div class="chips-row">
|
| 432 |
-
<button class="chip" onclick="sendQuery('Show me
|
| 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
|
|
|
|
|
|
|
| 436 |
</div>
|
| 437 |
|
| 438 |
<div id="chat-box" class="chat-box"></div>
|
| 439 |
|
| 440 |
<div class="input-row">
|
| 441 |
-
<input id="user-input" type="text" placeholder="Speak or type (e.g. '
|
| 442 |
<button id="send-btn" class="btn-send">Send</button>
|
| 443 |
</div>
|
| 444 |
</div>
|
|
@@ -469,6 +470,43 @@
|
|
| 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;
|
|
@@ -477,7 +515,6 @@
|
|
| 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'))
|
|
@@ -492,7 +529,6 @@
|
|
| 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];
|
|
@@ -549,6 +585,16 @@
|
|
| 549 |
return json.data || json;
|
| 550 |
}
|
| 551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
// PhoneLLM Conversational & Tool Calling Engine
|
| 553 |
async function processPhoneLLMTurn(userText) {
|
| 554 |
statusBadge.textContent = "โก PhoneLLM Reasoning...";
|
|
@@ -561,7 +607,22 @@
|
|
| 561 |
|
| 562 |
const numMatch = lower.match(/\b(?:comic|number|#)?\s*(\d+)\b/);
|
| 563 |
|
| 564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
toolName = "get_random_xkcd()";
|
| 566 |
statusBadge.textContent = "โ๏ธ Executing get_random_xkcd...";
|
| 567 |
comic = await toolGetRandomXkcd();
|
|
@@ -572,13 +633,13 @@
|
|
| 572 |
statusBadge.textContent = `โ๏ธ Executing get_xkcd_by_number(${comicNum})...`;
|
| 573 |
comic = await toolGetXkcdByNumber(comicNum);
|
| 574 |
replyText = `Here is XKCD comic #${comic.num}: "${comic.title}". The punchline alt text says: "${comic.alt}"`;
|
| 575 |
-
} else if (lower.includes("latest") || lower.includes("newest") || lower.includes("today") || lower.includes("recent")
|
| 576 |
toolName = "get_latest_xkcd()";
|
| 577 |
statusBadge.textContent = "โ๏ธ Executing get_latest_xkcd...";
|
| 578 |
comic = await toolGetLatestXkcd();
|
| 579 |
replyText = `Here is the latest XKCD comic: #${comic.num} titled "${comic.title}". The alt text says: "${comic.alt}"`;
|
| 580 |
} else {
|
| 581 |
-
replyText = `I am your XKCD voice assistant powered by PhoneLLM Alpha 1. You can ask me
|
| 582 |
}
|
| 583 |
|
| 584 |
addMessage(replyText, "agent", toolName);
|
|
@@ -676,7 +737,7 @@
|
|
| 676 |
|
| 677 |
// Initial greeting
|
| 678 |
setTimeout(() => {
|
| 679 |
-
addMessage("Hello! I am your Pipecat voice assistant powered by PhoneLLM Alpha 1.
|
| 680 |
}, 400);
|
| 681 |
</script>
|
| 682 |
</body>
|
|
|
|
| 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 topic search.</p>
|
| 412 |
</header>
|
| 413 |
|
| 414 |
<main class="main-grid">
|
|
|
|
| 429 |
</div>
|
| 430 |
|
| 431 |
<div class="chips-row">
|
| 432 |
+
<button class="chip" onclick="sendQuery('Show me a comic on Python programming language')">๐ Python Comic</button>
|
|
|
|
| 433 |
<button class="chip" onclick="sendQuery('What is XKCD comic 327 (Little Bobby Tables)?')">๐ป Little Bobby Tables (#327)</button>
|
| 434 |
+
<button class="chip" onclick="sendQuery('Show me a comic about Git version control')">๐ฟ Git Comic</button>
|
| 435 |
+
<button class="chip" onclick="sendQuery('Show me the latest XKCD comic')">โจ Latest Comic</button>
|
| 436 |
+
<button class="chip" onclick="sendQuery('Give me a random XKCD comic')">๐ฒ Random Comic</button>
|
| 437 |
</div>
|
| 438 |
|
| 439 |
<div id="chat-box" class="chat-box"></div>
|
| 440 |
|
| 441 |
<div class="input-row">
|
| 442 |
+
<input id="user-input" type="text" placeholder="Speak or type (e.g. 'Show me a comic on Python')..." />
|
| 443 |
<button id="send-btn" class="btn-send">Send</button>
|
| 444 |
</div>
|
| 445 |
</div>
|
|
|
|
| 470 |
let selectedVoice = null;
|
| 471 |
let availableVoices = [];
|
| 472 |
|
| 473 |
+
// Topic Knowledge Index for iconic XKCD comics
|
| 474 |
+
const TOPIC_INDEX = {
|
| 475 |
+
"python": 353,
|
| 476 |
+
"antigravity": 353,
|
| 477 |
+
"sql": 327,
|
| 478 |
+
"database": 327,
|
| 479 |
+
"injection": 327,
|
| 480 |
+
"bobby tables": 327,
|
| 481 |
+
"little bobby tables": 327,
|
| 482 |
+
"git": 1597,
|
| 483 |
+
"version control": 1597,
|
| 484 |
+
"tar": 1168,
|
| 485 |
+
"zip": 1168,
|
| 486 |
+
"archive": 1168,
|
| 487 |
+
"regex": 208,
|
| 488 |
+
"regular expression": 208,
|
| 489 |
+
"password": 936,
|
| 490 |
+
"correct horse": 936,
|
| 491 |
+
"machine learning": 1838,
|
| 492 |
+
"ai": 1838,
|
| 493 |
+
"artificial intelligence": 1838,
|
| 494 |
+
"compiling": 303,
|
| 495 |
+
"sword fight": 303,
|
| 496 |
+
"standards": 927,
|
| 497 |
+
"facebook": 300,
|
| 498 |
+
"social media": 300,
|
| 499 |
+
"wisdom of the ancients": 979,
|
| 500 |
+
"sudo": 149,
|
| 501 |
+
"sandwich": 149,
|
| 502 |
+
"lisp": 224,
|
| 503 |
+
"c++": 435,
|
| 504 |
+
"rust": 2248,
|
| 505 |
+
"javascript": 2248,
|
| 506 |
+
"linux": 272,
|
| 507 |
+
"kernel": 272
|
| 508 |
+
};
|
| 509 |
+
|
| 510 |
// Initialize High-Quality On-Device TTS Voices
|
| 511 |
function loadVoices() {
|
| 512 |
if (!('speechSynthesis' in window)) return;
|
|
|
|
| 515 |
|
| 516 |
voiceSelect.innerHTML = '';
|
| 517 |
|
|
|
|
| 518 |
const preferred = availableVoices.filter(v =>
|
| 519 |
v.lang.startsWith('en') &&
|
| 520 |
(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'))
|
|
|
|
| 529 |
voiceSelect.appendChild(opt);
|
| 530 |
});
|
| 531 |
|
|
|
|
| 532 |
selectedVoice = voicesToShow[0] || availableVoices[0];
|
| 533 |
voiceSelect.onchange = () => {
|
| 534 |
selectedVoice = voicesToShow[voiceSelect.value] || availableVoices[0];
|
|
|
|
| 585 |
return json.data || json;
|
| 586 |
}
|
| 587 |
|
| 588 |
+
async function toolSearchXkcdByTopic(topic) {
|
| 589 |
+
const cleanTopic = topic.toLowerCase().trim();
|
| 590 |
+
for (const [key, num] of Object.entries(TOPIC_INDEX)) {
|
| 591 |
+
if (cleanTopic.includes(key) || key.includes(cleanTopic)) {
|
| 592 |
+
return { comic: await toolGetXkcdByNumber(num), topic: key };
|
| 593 |
+
}
|
| 594 |
+
}
|
| 595 |
+
return { comic: await toolGetRandomXkcd(), topic: cleanTopic };
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
// PhoneLLM Conversational & Tool Calling Engine
|
| 599 |
async function processPhoneLLMTurn(userText) {
|
| 600 |
statusBadge.textContent = "โก PhoneLLM Reasoning...";
|
|
|
|
| 607 |
|
| 608 |
const numMatch = lower.match(/\b(?:comic|number|#)?\s*(\d+)\b/);
|
| 609 |
|
| 610 |
+
// Check for topic queries first (e.g. "comic on Python", "about git", "database comic")
|
| 611 |
+
let matchedTopic = null;
|
| 612 |
+
for (const key of Object.keys(TOPIC_INDEX)) {
|
| 613 |
+
if (lower.includes(key)) {
|
| 614 |
+
matchedTopic = key;
|
| 615 |
+
break;
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
if (matchedTopic && (lower.includes("about") || lower.includes("on") || lower.includes("show") || lower.includes("comic") || lower.includes("tell") || lower.includes("find") || lower.includes("give") || lower.includes("python") || lower.includes("git") || lower.includes("sql"))) {
|
| 620 |
+
toolName = `search_xkcd_by_topic(topic="${matchedTopic}")`;
|
| 621 |
+
statusBadge.textContent = `โ๏ธ Executing search_xkcd_by_topic("${matchedTopic}")...`;
|
| 622 |
+
const res = await toolSearchXkcdByTopic(matchedTopic);
|
| 623 |
+
comic = res.comic;
|
| 624 |
+
replyText = `Here is the iconic XKCD comic on ${matchedTopic.toUpperCase()}: #${comic.num} titled "${comic.title}". The punchline alt text says: "${comic.alt}"`;
|
| 625 |
+
} else if (lower.includes("random") || lower.includes("any comic")) {
|
| 626 |
toolName = "get_random_xkcd()";
|
| 627 |
statusBadge.textContent = "โ๏ธ Executing get_random_xkcd...";
|
| 628 |
comic = await toolGetRandomXkcd();
|
|
|
|
| 633 |
statusBadge.textContent = `โ๏ธ Executing get_xkcd_by_number(${comicNum})...`;
|
| 634 |
comic = await toolGetXkcdByNumber(comicNum);
|
| 635 |
replyText = `Here is XKCD comic #${comic.num}: "${comic.title}". The punchline alt text says: "${comic.alt}"`;
|
| 636 |
+
} else if (lower.includes("latest") || lower.includes("newest") || lower.includes("today") || lower.includes("recent")) {
|
| 637 |
toolName = "get_latest_xkcd()";
|
| 638 |
statusBadge.textContent = "โ๏ธ Executing get_latest_xkcd...";
|
| 639 |
comic = await toolGetLatestXkcd();
|
| 640 |
replyText = `Here is the latest XKCD comic: #${comic.num} titled "${comic.title}". The alt text says: "${comic.alt}"`;
|
| 641 |
} else {
|
| 642 |
+
replyText = `I am your XKCD voice assistant powered by PhoneLLM Alpha 1. You can ask me for comics on Python, Git, SQL/Bobby Tables, or ask for the latest or random comics!`;
|
| 643 |
}
|
| 644 |
|
| 645 |
addMessage(replyText, "agent", toolName);
|
|
|
|
| 737 |
|
| 738 |
// Initial greeting
|
| 739 |
setTimeout(() => {
|
| 740 |
+
addMessage("Hello! I am your Pipecat voice assistant powered by PhoneLLM Alpha 1. Try asking: 'Show me a comic on Python' or 'What is Little Bobby Tables?'", "agent");
|
| 741 |
}, 400);
|
| 742 |
</script>
|
| 743 |
</body>
|