The AI DJ lives in src/ai-chat.ts and renders into a single <aside class="aichat__panel"> element appended to <body> from src/main.ts. It is a vanilla TypeScript module — no framework, no shadow DOM. The same panel handles the FAB, the conversation list, the composer, slash commands, streaming, voice input, message tools, and the new rich-widget renderer.
- Floating action button — bottom-right
[data-aichat="fab"]. Hidden via CSS when the panel is open. Restores focus on close. - Side panel —
[data-aichat="panel"]. Slides in from the right on desktop, near-fullscreen on phones.Cmd/Ctrl + Itoggles.Escapecloses. - Sessions — multi-conversation list persisted to
localStorageunderbz:aichat:state. Rename, pin, delete, branch, export to Markdown. - Streaming replies — SSE from
POST /api/ai/chat. Worker streams Anthropic Claude responses; client renders tokens incrementally with a blinking caret. - Slash commands —
/help,/clear,/copy,/persona <name>,/play,/pause,/viz <mode>,/catalog,/track <id>,/album <id>,/shortcommands, … - Rich widgets — assistant messages can carry a
widgets[]array of typed payloads (track cards, command palettes, photos, pricing tiers, alerts, citations, …). Seeai-chat-widgets.md. - Voice — Web Speech API push-to-talk. Wake-word listener ("hey bZ") is opt-in via the settings drawer.
- Search —
Cmd/Ctrl + Fopens an inline search bar that highlights matching messages in the current session.
The Worker reads ANTHROPIC_API_KEY from Wrangler secrets:
npx wrangler secret put ANTHROPIC_API_KEY --env productionWhen the secret is missing, POST /api/ai/chat responds 503 {"error":"ai_not_configured"} and the client surfaces a friendly inline notice. The panel still works as a local UI: slash commands that don't need the model (/help, /clear, /catalog, /track, /album, /shortcommands, …) all run client-side.
- Conversations live in
localStorageonly. Nothing is uploaded unless the user posts a message. - The Worker does not log message bodies; it forwards them to Anthropic and streams the response back. Add request-level logging only when explicitly debugging.
- The voice transcription runs in the user's browser via Web Speech API — audio never leaves the device.
/clearwipes the active session immediately;/forgetwipes every session and reloads the panel.
- Every widget URL goes through
safeUrl()(src/ai-widgets.ts).javascript:,data:,file:,blob:, and protocol-relative//evil.comURLs are rewritten to#. - Every widget string runs through
escapeHtml()before injection. The widget renderer is a pure HTML-string builder; it never touchesinnerHTMLwith raw input. - Click delegation in
messages.addEventListener('click', …)routes[data-aichat-cmd]buttons throughhandleSlash('/'+cmd)so palette clicks reuse the same audit path as typed commands. - Destructive commands (
/clear,/forget,/reset) confirm viasetStatus()before mutating local state.
- Decide whether it belongs in the worker (network call, secrets, persistence) or the client (UI, local state).
- For a new slash command see
ai-chat-commands.md. - For a new rich response shape see
ai-chat-widgets.md. - Update
tests/journey.spec.ts— every new user-visible affordance gets at least one Playwright assertion against the production-served bundle. - Run
npm run build && npx vitest run && npx tsc --noEmit && npx prettier --write src/ai-chat.ts src/ai-widgets.ts src/ai-shortcommands.tsbefore opening a PR.