Skip to content

Commit 68c2c87

Browse files
authored
Merge pull request #193 from idvorkin/add-vapi-widget
Add Vapi.ai voice widget to Tesla page for Tony AI coach
2 parents 665f47d + dd5132c commit 68c2c87

File tree

4 files changed

+163
-2
lines changed

4 files changed

+163
-2
lines changed

CLAUDE.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,50 @@ Use the `quadrant-matrix.html` include for any 2x2 grid visualization:
337337

338338
Common uses: PANAS personalities, Eisenhower matrix, SWOT analysis, risk assessment. Quadrants numbered clockwise from top-right.
339339

340+
### Vapi.ai Voice/Chat Widget
341+
342+
For adding interactive AI voice or chat capabilities (like Tony the AI life coach), use the Vapi.ai web widget:
343+
344+
**Documentation:** [https://docs.vapi.ai/chat/web-widget](https://docs.vapi.ai/chat/web-widget)
345+
346+
**Implementation pattern (see `_d/tesla.md` for live example):**
347+
348+
```html
349+
<script
350+
src="https://unpkg.com/@vapi-ai/client-sdk-react/dist/embed/widget.umd.js"
351+
async
352+
></script>
353+
354+
<vapi-widget
355+
public-key="49b277de-d508-4062-bec2-503e40915be4"
356+
assistant-id="f5fe3b31-0ff6-4395-bc08-bc8ebbbf48a6"
357+
mode="chat"
358+
theme="dark"
359+
main-label="Talk to Tony"
360+
base-color="#2c2c2c"
361+
accent-color="#c0392b"
362+
></vapi-widget>
363+
```
364+
365+
**Configuration options:**
366+
367+
- `mode`: "chat" (text-based) or "voice" (voice-based)
368+
- `theme`: "light" or "dark"
369+
- `main-label`: Button/widget label text
370+
- `base-color`: Background color (hex or rgba)
371+
- `accent-color`: Accent/highlight color (hex or rgba)
372+
373+
**Credentials:**
374+
375+
- Public key and assistant ID are stored in the widget code
376+
- These are public-facing and safe to commit to the repo
377+
- Backend logic/API keys are managed separately in the tony_tesla project
378+
379+
**Related projects:**
380+
381+
- Backend: [github.com/idvorkin/tony_tesla](https://github.com/idvorkin/tony_tesla)
382+
- Blog integration: See `/tesla` page for reference implementation
383+
340384
## Internal Link Guidelines
341385

342386
**IMPORTANT**: Always use permalinks when linking between blog posts, not redirect URLs:

_d/tesla.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ layout: post
33
title: "Tony (Tessie) the Tesla"
44
permalink: /tesla
55
imagefeature: https://github.com/idvorkin/blob/raw/master/blog/tesla-juggle.jpg
6+
redirect_from:
7+
- /tony-play
8+
- /tony
9+
- /talk-to-tony
10+
- /chat-tony
611
---
712

813
I don't like to drive. I don't like to drive so much that I have it in my eulogy. Luckily, Elon Musk made a car that's optimized for driving you. In August 2023, I treated myself to a Tesla Model Y. It has lots of trade-offs, but the key for me is its Autopilot (e.g., it driving me).
@@ -11,6 +16,26 @@ This created quite the identity crisis for someone who prided himself on being a
1116

1217
{% include summarize-page.html src="/bike-tesla-identity" %}
1318

19+
### Talk to Tony - My AI Life Coach
20+
21+
Tony is my callable AI life coach with a Tony Soprano personality. I named him after my Tesla, and he's got that same no-nonsense, direct approach to getting you where you need to go. He's particularly good at identity conflicts, rationalization detection, and keeping you accountable to what you actually care about.
22+
23+
<script src="https://unpkg.com/@vapi-ai/client-sdk-react/dist/embed/widget.umd.js" async></script>
24+
25+
<!-- prettier-ignore-start -->
26+
<vapi-widget
27+
public-key="49b277de-d508-4062-bec2-503e40915be4"
28+
assistant-id="f5fe3b31-0ff6-4395-bc08-bc8ebbbf48a6"
29+
mode="chat"
30+
theme="dark"
31+
main-label="Talk to Tony"
32+
base-color="#2c2c2c"
33+
accent-color="#c0392b"
34+
></vapi-widget>
35+
<!-- prettier-ignore-end -->
36+
37+
Want to build your own AI coach? Check out my [AI Bestie post](/ai-bestie) for the technical details. The Tony project lives at [github.com/idvorkin/tony_tesla](https://github.com/idvorkin/tony_tesla).
38+
1439
{% include link-blog-montage.html week="687" %}
1540

1641
{% include blob_image_float_right.html src="blog/tesla-juggle.jpg" %}

back-links.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@
699699
"file_path": "_site/ai-bestie.html",
700700
"incoming_links": [
701701
"/ai",
702+
"/tesla",
702703
"/y24"
703704
],
704705
"last_modified": "2025-08-22T11:54:12-07:00",
@@ -5340,7 +5341,7 @@
53405341
},
53415342
"/tesla": {
53425343
"description": "I don’t like to drive. I don’t like to drive so much that I have it in my eulogy. Luckily, Elon Musk made a car that’s optimized for driving you. In August 2023, I treated myself to a Tesla Model Y. It has lots of trade-offs, but the key for me is its Autopilot (e.g., it driving me).\n\n",
5343-
"doc_size": 19000,
5344+
"doc_size": 20000,
53445345
"file_path": "_site/tesla.html",
53455346
"incoming_links": [
53465347
"/bucket-list",
@@ -5351,9 +5352,10 @@
53515352
"/operating-manual",
53525353
"/timeoff-2024-02"
53535354
],
5354-
"last_modified": "2025-09-14T17:30:06-07:00",
5355+
"last_modified": "2025-11-17T00:13:59.906977",
53555356
"markdown_path": "_d/tesla.md",
53565357
"outgoing_links": [
5358+
"/ai-bestie",
53575359
"/bike-tesla-identity"
53585360
],
53595361
"redirect_url": "",

tests/e2e/vapi-widget.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test.describe("Vapi Widget on Tesla Page", () => {
4+
test("should load Vapi widget script and render widget element", async ({ page }) => {
5+
// Go to the tesla page
6+
await page.goto("http://localhost:4000/tesla");
7+
8+
// Wait for page to fully load
9+
await page.waitForLoadState("networkidle");
10+
await page.waitForTimeout(1000); // Give extra time for JS to run
11+
12+
// Log console messages for debugging
13+
page.on("console", (msg) => {
14+
if (msg.text().includes("vapi") || msg.text().includes("widget")) {
15+
console.log("🎙️ Console:", msg.text());
16+
}
17+
});
18+
19+
// Check if the Vapi script is loaded
20+
const hasVapiScript = await page.evaluate(() => {
21+
const scripts = Array.from(document.scripts);
22+
return scripts.some((script) => script.src.includes("@vapi-ai/client-sdk-react"));
23+
});
24+
25+
console.log(`Vapi script loaded: ${hasVapiScript}`);
26+
expect(hasVapiScript).toBe(true);
27+
28+
// Check if the vapi-widget custom element exists in the DOM
29+
const vapiWidget = page.locator("vapi-widget");
30+
const widgetCount = await vapiWidget.count();
31+
console.log(`Vapi widget elements found: ${widgetCount}`);
32+
expect(widgetCount).toBeGreaterThan(0);
33+
34+
// Verify widget has the expected attributes
35+
const publicKey = await vapiWidget.getAttribute("public-key");
36+
const assistantId = await vapiWidget.getAttribute("assistant-id");
37+
const mode = await vapiWidget.getAttribute("mode");
38+
const theme = await vapiWidget.getAttribute("theme");
39+
const mainLabel = await vapiWidget.getAttribute("main-label");
40+
41+
console.log("Widget attributes:");
42+
console.log(` - public-key: ${publicKey ? "✓ present" : "✗ missing"}`);
43+
console.log(` - assistant-id: ${assistantId ? "✓ present" : "✗ missing"}`);
44+
console.log(` - mode: ${mode}`);
45+
console.log(` - theme: ${theme}`);
46+
console.log(` - main-label: ${mainLabel}`);
47+
48+
expect(publicKey).toBe("49b277de-d508-4062-bec2-503e40915be4");
49+
expect(assistantId).toBe("f5fe3b31-0ff6-4395-bc08-bc8ebbbf48a6");
50+
expect(mode).toBe("chat");
51+
expect(theme).toBe("dark");
52+
expect(mainLabel).toBe("Talk to Tony");
53+
});
54+
55+
test("verify widget has correct styling attributes", async ({ page }) => {
56+
await page.goto("http://localhost:4000/tesla");
57+
await page.waitForLoadState("networkidle");
58+
await page.waitForTimeout(1000);
59+
60+
const vapiWidget = page.locator("vapi-widget");
61+
62+
// Check color attributes
63+
const baseColor = await vapiWidget.getAttribute("base-color");
64+
const accentColor = await vapiWidget.getAttribute("accent-color");
65+
66+
console.log("Widget styling:");
67+
console.log(` - base-color: ${baseColor}`);
68+
console.log(` - accent-color: ${accentColor}`);
69+
70+
expect(baseColor).toBe("#2c2c2c");
71+
expect(accentColor).toBe("#c0392b");
72+
});
73+
74+
test("verify widget appears in Talk to Tony section", async ({ page }) => {
75+
await page.goto("http://localhost:4000/tesla");
76+
await page.waitForLoadState("networkidle");
77+
78+
// Check that the section header exists
79+
const tonyHeader = page.locator("h3:has-text('Talk to Tony')");
80+
const headerExists = (await tonyHeader.count()) > 0;
81+
console.log(`"Talk to Tony" header found: ${headerExists}`);
82+
expect(headerExists).toBe(true);
83+
84+
// Check that widget is near the header
85+
const widgetNearHeader = page.locator("h3:has-text('Talk to Tony') ~ vapi-widget");
86+
const widgetFound = (await widgetNearHeader.count()) > 0;
87+
console.log(`Widget found after "Talk to Tony" header: ${widgetFound}`);
88+
expect(widgetFound).toBe(true);
89+
});
90+
});

0 commit comments

Comments
 (0)