From 10b8feb9ea0b70c409806ca11999f3d3768df2e7 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Thu, 21 May 2026 22:53:38 -0400 Subject: [PATCH] Add shopping-list UIWorker example (bridge-free voice + UI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demonstrates the 'every input acts, may speak' pattern without bridging: a standard voice pipeline (STT → LLM → TTS) whose LLM only converses, plus a separate UIWorker that does all the list work. The voice pipeline's user aggregator fires on_user_turn_stopped each turn and dispatches the transcript to the UIWorker as a respond job (a bus message); the UIWorker reads the auto-injected snapshot and drives the list silently via add_item / set_checked / remove_item commands (plus the standard highlight). Items are checkboxes whose label and checked state the snapshot exposes. Includes a vanilla-JS client following the existing UI-demo client style. --- .../ui-worker/shopping-list/README.md | 92 ++ .../ui-worker/shopping-list/bot.py | 325 +++++ .../ui-worker/shopping-list/client/.gitignore | 26 + .../ui-worker/shopping-list/client/index.html | 44 + .../ui-worker/shopping-list/client/main.js | 230 ++++ .../shopping-list/client/package-lock.json | 1128 +++++++++++++++++ .../shopping-list/client/package.json | 18 + .../ui-worker/shopping-list/client/styles.css | 216 ++++ .../shopping-list/client/vite.config.js | 7 + 9 files changed, 2086 insertions(+) create mode 100644 examples/multi-worker/ui-worker/shopping-list/README.md create mode 100644 examples/multi-worker/ui-worker/shopping-list/bot.py create mode 100644 examples/multi-worker/ui-worker/shopping-list/client/.gitignore create mode 100644 examples/multi-worker/ui-worker/shopping-list/client/index.html create mode 100644 examples/multi-worker/ui-worker/shopping-list/client/main.js create mode 100644 examples/multi-worker/ui-worker/shopping-list/client/package-lock.json create mode 100644 examples/multi-worker/ui-worker/shopping-list/client/package.json create mode 100644 examples/multi-worker/ui-worker/shopping-list/client/styles.css create mode 100644 examples/multi-worker/ui-worker/shopping-list/client/vite.config.js diff --git a/examples/multi-worker/ui-worker/shopping-list/README.md b/examples/multi-worker/ui-worker/shopping-list/README.md new file mode 100644 index 000000000..b52592174 --- /dev/null +++ b/examples/multi-worker/ui-worker/shopping-list/README.md @@ -0,0 +1,92 @@ +# shopping-list + +Every voice turn drives the UI; speech is incidental. The user builds a +shopping list by talking — "add milk and eggs", "check off the bread", +"drop the last one", "what's left?" — and the list updates on screen +every turn. The assistant may also say something back. The two halves run +in parallel on separate workers; neither calls the other as a tool. + +This is the **bridge-free** pattern for "every input acts, may speak." + +## What it shows + +- **A standard voice pipeline + a UIWorker, no bridging.** The voice + layer is an ordinary `transport → STT → LLM → TTS` pipeline whose LLM + just converses (no tools, never touches the list). Its user aggregator + fires `on_user_turn_stopped` once per user turn, and that handler + dispatches the transcript to the UIWorker as a `respond` job + (`worker.job("ui", name="respond", payload={"query": transcript})`) — + a bus message. The UIWorker does all the list work, silently, on its + own worker, so its LLM output never reaches TTS. +- **Snapshot-driven action.** Before each UIWorker inference, the current + `` is auto-injected (via the LLM's `on_before_process_frame` + hook), so the worker resolves "the milk", "the last one", "the checked + ones" against the live list. +- **Custom UI commands.** A single bundled `update_list` tool maps the + request to `add_item` / `set_checked` / `remove_item` commands (plus the + standard `highlight`, used to *show* what's left). Each item is a + checkbox whose accessible name is the item text, so the snapshot exposes + every item's label and checked state. + +## What it adds vs. the prior demos + +The other demos use the **request/response delegation** shape: a voice LLM +decides, via a tool call, when to consult the UIWorker. This one removes +that decision — **every** user turn goes to the UIWorker automatically, +and the voice LLM runs independently for conversation. The wiring is a +single `on_user_turn_stopped` handler instead of a delegating tool. + +## Run + +Two terminals. + +**Terminal 1 — bot:** + +```bash +cd examples/multi-worker/ui-worker/shopping-list +uv run python bot.py +``` + +The bot starts on `http://localhost:7860`. + +**Terminal 2 — client:** + +```bash +cd examples/multi-worker/ui-worker/shopping-list/client +npm install # one-time +npm run dev +``` + +Open `http://localhost:5173` and click **Connect**. + +## What to try + +- _"Add milk and a dozen eggs."_ — both items appear; the voice + acknowledges. +- _"Add bread, butter, and coffee."_ — three more land. +- _"Check off the bread."_ — it gets ticked and struck through. +- _"Actually, drop the butter."_ — removed. +- _"Clear the ones I've already got."_ — removes everything checked. +- _"What's left?"_ — the unchecked items pulse, and the voice tells you. +- _"Hi there!"_ — the voice greets you; the list is unchanged (the + UIWorker no-ops). + +You can also type in the box to add items by hand — they go through the +same snapshot path, so the worker sees them next turn. + +## Requirements + +- `OPENAI_API_KEY` +- `DEEPGRAM_API_KEY` +- `CARTESIA_API_KEY` + +A `.env` in the example folder is the easiest way to set these (see +`examples/multi-worker/env.example`). + +## What this example _doesn't_ show + +The voice layer is **blind to the screen** — it converses and answers from +the dialogue so far, but it doesn't read the live list (only the UIWorker +sees the snapshot). For "what's left?" the UIWorker highlights the answer +visually while the voice gives a from-memory summary. The list is also not +persisted — refresh and it's gone. diff --git a/examples/multi-worker/ui-worker/shopping-list/bot.py b/examples/multi-worker/ui-worker/shopping-list/bot.py new file mode 100644 index 000000000..067fa4479 --- /dev/null +++ b/examples/multi-worker/ui-worker/shopping-list/bot.py @@ -0,0 +1,325 @@ +# +# Copyright (c) 2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Shopping list — every voice turn drives the UI; speech is incidental. + +The page renders a shopping list. The user talks: "add milk and eggs", +"check off the bread", "drop the last one", "what's left?". Every user +turn updates the list on screen; the assistant may also say something +back. The two halves run in parallel on separate workers — neither calls +the other as a tool. + +This is the bridge-free pattern for "every input acts, may speak": + +- The **voice layer** is an ordinary voice pipeline (STT → LLM → TTS). + Its LLM just converses — it has no tools and never touches the list. +- A **UIWorker** ("ui") does all the list work. It is *not* in the voice + pipeline. Instead, the voice pipeline's user aggregator fires + ``on_user_turn_stopped`` once per user turn; that handler dispatches + the transcript to the UIWorker as a ``respond`` job (a bus message). + The UIWorker reads the current ```` snapshot (auto-injected + before its inference) and calls ``update_list`` to add / check / + remove items. It acts silently — its LLM output never reaches TTS, + because it lives on its own worker. + +Architecture:: + + Voice pipeline (PipelineWorker "main", owns transport + RTVI): + transport.in → STT → user_agg → LLM → TTS → transport.out → assistant_agg + └── @on_user_turn_stopped: worker.job("ui", name="respond", + payload={"query": transcript}) + + UIWorker "ui" (job-based, acts silently): + └── @tool update_list(add, check, uncheck, remove, highlight) + └── send_command("add_item" / "set_checked" / "remove_item") + + respond_to_job() # no speak + +Run:: + + uv run python bot.py + +Then open the client at ``http://localhost:5173`` (see ``README.md``). + +Requirements: + +- OPENAI_API_KEY +- DEEPGRAM_API_KEY +- CARTESIA_API_KEY +""" + +import os + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame +from pipecat.pipeline.job_context import JobError +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.worker import PipelineParams, PipelineWorker +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.llm_service import FunctionCallParams +from pipecat.services.openai.llm import OpenAILLMService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.workers.llm import tool +from pipecat.workers.ui import UIWorker + +load_dotenv(override=True) + +MAIN_NAME = "main" +UI_NAME = "ui" + +transport_params = { + "daily": lambda: DailyParams(audio_in_enabled=True, audio_out_enabled=True), + "webrtc": lambda: TransportParams(audio_in_enabled=True, audio_out_enabled=True), +} + + +VOICE_PROMPT = """\ +You are the voice of a shopping-list assistant. A separate UI layer \ +sees the list and updates it on screen; you do NOT touch the list \ +yourself and you cannot see the screen. + +Your job is to be a brief, friendly companion. Acknowledge what the \ +user asked for ("Sure, adding milk and eggs"), react naturally, and \ +answer questions from the conversation so far ("So far you've asked \ +for milk, eggs, and bread"). Keep every reply to one short spoken \ +sentence. Don't describe how you're updating the list — the screen \ +shows that. For a plain greeting, greet back warmly.""" + + +# The UI wire-format guide (UI_STATE_PROMPT_GUIDE) is appended to the LLM's +# system instruction automatically by UIWorker, so this prompt only needs the +# app-specific behavior. +UI_PROMPT = """\ +You maintain a shopping list by voice. The current ```` \ +block is in your context. Each list item is a checkbox with a \ +snapshot ref (e.g. ``e5``), a label (the item text), and a checked / \ +unchecked state. Use the labels to resolve which item the user means. + +## Tool: update_list + +Every turn calls ``update_list`` exactly once — even when there's \ +nothing to change (call it with no arguments). One tool call per turn. + +``update_list(add=None, check=None, uncheck=None, remove=None, highlight=None)``: + +- ``add`` (OPTIONAL): a list of item texts to append. Split a spoken \ +list into separate items ("milk, eggs and bread" → \ +``["milk", "eggs", "bread"]``). Normalize lightly (lowercase, drop \ +filler like "some" / "a couple of"). +- ``check`` (OPTIONAL): a list of refs to mark done. +- ``uncheck`` (OPTIONAL): a list of refs to mark not done. +- ``remove`` (OPTIONAL): a list of refs to delete. +- ``highlight`` (OPTIONAL): a list of refs to flash briefly. Use it to \ +*show* the user something when they ask (e.g. highlight the unchecked \ +items for "what's left?"). + +## Decision rules + +- **"Add X (and Y)"** → ``add`` with one entry per item. +- **"Check off / got / cross out X"** → resolve X's ref from \ +````, set ``check``. +- **"Uncheck / put back X"** → set ``uncheck``. +- **"Remove / delete / never mind X"**, **"clear the checked ones"** → \ +set ``remove`` with the matching refs (read the checked state from \ +```` for "the checked ones"). +- **"the last one"** → the last item in ````. +- **"What's left? / what do I still need?"** → ``highlight`` the \ +unchecked items. **"What's on my list?"** → ``highlight`` everything. \ +(The voice layer speaks; you just point.) +- **A greeting or anything not about the list** → call ``update_list`` \ +with no arguments (no-op) so the turn completes. + +## Examples + +(refs are illustrative; use the actual refs from the current \ +````) + +- "Add milk and a dozen eggs." → \ +``update_list(add=["milk", "eggs"])`` +- "Check off the milk." → ``update_list(check=["e5"])`` +- "Actually drop the eggs." → ``update_list(remove=["e7"])`` +- "Clear the ones I've got." (e5, e9 checked) → \ +``update_list(remove=["e5", "e9"])`` +- "What's left?" (e7, e11 unchecked) → \ +``update_list(highlight=["e7", "e11"])`` +- "Hey there." → ``update_list()``""" + + +class ListWorker(UIWorker): + """UIWorker that maintains the shopping list, silently. + + A single bundled ``update_list`` tool, always called once per turn, + maps the user's request to ``add_item`` / ``set_checked`` / + ``remove_item`` UI commands (plus the standard ``highlight``). The + tool completes the ``respond`` job with no spoken reply + (``respond_to_job()`` with no ``speak``) — the separate voice layer + owns speech. + """ + + def __init__(self): + llm = OpenAILLMService( + api_key=os.environ["OPENAI_API_KEY"], + settings=OpenAILLMService.Settings(system_instruction=UI_PROMPT), + ) + super().__init__(UI_NAME, llm=llm) + + @tool + async def update_list( + self, + params: FunctionCallParams, + add: list[str] | None = None, + check: list[str] | None = None, + uncheck: list[str] | None = None, + remove: list[str] | None = None, + highlight: list[str] | None = None, + ): + """Update the shopping list. Called exactly once per turn. + + Args: + add: New item texts to append (one entry per item). + check: Snapshot refs of items to mark done. + uncheck: Snapshot refs of items to mark not done. + remove: Snapshot refs of items to delete. + highlight: Snapshot refs of items to flash briefly (e.g. to + show what's left when the user asks). + """ + logger.info( + f"{self}: update_list(add={add!r}, check={check!r}, uncheck={uncheck!r}, " + f"remove={remove!r}, highlight={highlight!r})" + ) + # Defensive guards: skip malformed entries so a stray value can't + # crash the tool before respond_to_job fires (which would hold the + # single-flight lock until the requester's timeout). + for text in add or []: + if isinstance(text, str) and text.strip(): + await self.send_command("add_item", {"text": text.strip()}) + for ref in check or []: + if isinstance(ref, str): + await self.send_command("set_checked", {"ref": ref, "checked": True}) + for ref in uncheck or []: + if isinstance(ref, str): + await self.send_command("set_checked", {"ref": ref, "checked": False}) + for ref in remove or []: + if isinstance(ref, str): + await self.send_command("remove_item", {"ref": ref}) + for ref in highlight or []: + if isinstance(ref, str): + await self.highlight(ref) + await self.respond_to_job() + await params.result_callback(None) + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info("Starting shopping-list bot") + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"]) + tts = CartesiaTTSService( + api_key=os.environ["CARTESIA_API_KEY"], + settings=CartesiaTTSService.Settings( + voice=os.getenv("CARTESIA_VOICE_ID", "71a7ad14-091c-4e8e-a314-022ece01c121"), + ), + ) + llm = OpenAILLMService( + api_key=os.environ["OPENAI_API_KEY"], + settings=OpenAILLMService.Settings(system_instruction=VOICE_PROMPT), + ) + + context = LLMContext() + aggregators = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + user_aggregator = aggregators.user() + assistant_aggregator = aggregators.assistant() + + pipeline = Pipeline( + [ + transport.input(), + stt, + user_aggregator, + llm, + tts, + transport.output(), + assistant_aggregator, + ] + ) + + worker = PipelineWorker( + pipeline, + name=MAIN_NAME, + params=PipelineParams(enable_metrics=True, enable_usage_metrics=True), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + # Every user turn drives the UI: forward the transcript to the UIWorker + # as a respond job (a bus message). Fire it from the turn-stopped event + # so the voice LLM (which runs from the same turn) and the UIWorker act + # in parallel — neither calls the other as a tool. This handler runs in + # its own task, so awaiting the job here does not block the voice path. + @user_aggregator.event_handler("on_user_turn_stopped") + async def on_user_turn_stopped(aggregator, strategy, message): + transcript = (message.content or "").strip() + if not transcript: + return + logger.info(f"Dispatching turn to UI worker: {transcript!r}") + try: + async with worker.job( + UI_NAME, name="respond", payload={"query": transcript}, timeout=15 + ): + pass + except JobError as e: + logger.warning(f"ui job failed: {e}") + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info("Client connected") + context.add_message( + { + "role": "developer", + "content": ( + "Greet the user briefly. Tell them they can build their " + "shopping list by voice — add items, check things off, or " + "ask what's left. One short sentence." + ), + } + ) + await worker.queue_frame(LLMRunFrame()) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info("Client disconnected") + await runner.cancel() + + await runner.launch_worker(ListWorker()) + await runner.launch_worker(worker) + + await runner.run() + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/multi-worker/ui-worker/shopping-list/client/.gitignore b/examples/multi-worker/ui-worker/shopping-list/client/.gitignore new file mode 100644 index 000000000..503bb516c --- /dev/null +++ b/examples/multi-worker/ui-worker/shopping-list/client/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.vite diff --git a/examples/multi-worker/ui-worker/shopping-list/client/index.html b/examples/multi-worker/ui-worker/shopping-list/client/index.html new file mode 100644 index 000000000..9924aefc9 --- /dev/null +++ b/examples/multi-worker/ui-worker/shopping-list/client/index.html @@ -0,0 +1,44 @@ + + + + + + Shopping list — Pipecat UIWorker + + + +
+

🛒 Shopping list

+
+ + +
+
+ +
+

+ Talk to build your list — "add milk and eggs", "check off the + bread", "what's left?". You can also type an item below. +

+ +
+ + +
+ +
    +

    + Your list is empty. Say "add milk and eggs". +

    +
    + + + + + diff --git a/examples/multi-worker/ui-worker/shopping-list/client/main.js b/examples/multi-worker/ui-worker/shopping-list/client/main.js new file mode 100644 index 000000000..77b6151a2 --- /dev/null +++ b/examples/multi-worker/ui-worker/shopping-list/client/main.js @@ -0,0 +1,230 @@ +/** + * Shopping list — vanilla JS client. + * + * The bridge-free "every input acts, may speak" pattern: + * + * - A standard voice pipeline runs on the bot. Its LLM converses (you + * hear it), and its user aggregator forwards each transcript to a + * separate UIWorker. + * - The UIWorker reads the snapshot of THIS page and drives the list + * silently via three custom commands — ``add_item``, ``set_checked``, + * ``remove_item`` — plus the standard ``highlight`` (used to show + * "what's left"). + * + * Each item is a checkbox with its text as the accessible name, so the + * snapshot exposes every item's label and checked state to the worker. + */ + +import { + PipecatClient, + RTVIEvent, + findElementByRef, +} from "@pipecat-ai/client-js"; +import { SmallWebRTCTransport } from "@pipecat-ai/small-webrtc-transport"; + +const BOT_URL = "http://localhost:7860/api/offer"; + +const connectButton = document.getElementById("connect"); +const status = document.getElementById("status"); +const botAudio = document.getElementById("bot-audio"); +const list = document.getElementById("list"); +const listEmpty = document.getElementById("list-empty"); +const addForm = document.getElementById("add-form"); +const addInput = document.getElementById("add-input"); + +let client; +let unsubscribes = []; + +function setStatus(text, autoHideMs = 0) { + status.textContent = text; + status.dataset.show = text ? "1" : "0"; + if (text && autoHideMs > 0) { + setTimeout(() => { + if (status.textContent === text) status.dataset.show = "0"; + }, autoHideMs); + } +} + +function refreshEmptyState() { + listEmpty.hidden = list.children.length > 0; +} + +// Render one item. The checkbox's accessible name is the item text, so +// the snapshot walker exposes each item as a checkbox with its label and +// checked state — that's what the UIWorker reads to resolve "the milk", +// "the last one", "the checked ones", etc. +function addItem(text, checked = false) { + const trimmed = String(text ?? "").trim(); + if (!trimmed) return; + + const li = document.createElement("li"); + li.className = "item"; + + const label = document.createElement("label"); + const cb = document.createElement("input"); + cb.type = "checkbox"; + cb.checked = !!checked; + cb.setAttribute("aria-label", trimmed); + cb.addEventListener("change", () => { + li.classList.toggle("checked", cb.checked); + }); + + const span = document.createElement("span"); + span.className = "item-text"; + span.textContent = trimmed; + + label.appendChild(cb); + label.appendChild(span); + li.appendChild(label); + li.classList.toggle("checked", !!checked); + + list.appendChild(li); + li.classList.add("item-flash"); + setTimeout(() => li.classList.remove("item-flash"), 1000); + refreshEmptyState(); +} + +function checkboxFor(ref) { + const el = ref ? findElementByRef(ref) : null; + if (!el) return null; + if (el instanceof HTMLInputElement) return el; + return el.querySelector?.("input[type=checkbox]") ?? null; +} + +// ───────────────────────────────────────────── +// Command handlers +// ───────────────────────────────────────────── + +function handleAddItem(payload) { + addItem(payload?.text); +} + +function handleSetChecked(payload) { + const cb = checkboxFor(payload?.ref); + if (!cb) return; + cb.checked = payload?.checked !== false; + cb.dispatchEvent(new Event("change", { bubbles: true })); +} + +function handleRemoveItem(payload) { + const cb = checkboxFor(payload?.ref); + const li = cb?.closest(".item"); + if (!li) return; + li.remove(); + refreshEmptyState(); +} + +// Standard ``highlight`` command: pulse the item row(s). The server may +// send one ref per command; we flash the enclosing item. +function handleHighlight(payload) { + const cb = checkboxFor(payload?.ref); + const row = cb?.closest(".item"); + if (!row) return; + const duration = payload?.duration_ms ?? 1500; + row.style.setProperty("--highlight-duration", `${duration}ms`); + row.classList.remove("ui-highlight"); + void row.offsetWidth; // restart the animation if already running + row.classList.add("ui-highlight"); + setTimeout(() => { + row.classList.remove("ui-highlight"); + row.style.removeProperty("--highlight-duration"); + }, duration); +} + +function onUICommand(command, handler) { + const listener = (data) => { + if (data.command !== command) return; + handler(data.payload); + }; + client.on(RTVIEvent.UICommand, listener); + return () => client.off(RTVIEvent.UICommand, listener); +} + +// Manual add (works without voice, and exercises the same snapshot path). +addForm.addEventListener("submit", (e) => { + e.preventDefault(); + addItem(addInput.value); + addInput.value = ""; +}); + +// ───────────────────────────────────────────── +// Connection lifecycle +// ───────────────────────────────────────────── + +async function connect() { + connectButton.disabled = true; + setStatus("Connecting…"); + + client = new PipecatClient({ + transport: new SmallWebRTCTransport(), + enableMic: true, + enableCam: false, + }); + + client.on(RTVIEvent.BotConnected, () => setStatus("Bot connected", 1500)); + client.on(RTVIEvent.Disconnected, () => { + setStatus("Disconnected", 2000); + connectButton.dataset.state = ""; + connectButton.textContent = "Connect"; + connectButton.disabled = false; + teardownUI(); + }); + + client.on(RTVIEvent.TrackStarted, (track, participant) => { + if (track.kind !== "audio") return; + if (participant?.local) return; + botAudio.srcObject = new MediaStream([track]); + }); + + unsubscribes = [ + onUICommand("add_item", handleAddItem), + onUICommand("set_checked", handleSetChecked), + onUICommand("remove_item", handleRemoveItem), + onUICommand("highlight", handleHighlight), + ]; + + try { + await client.connect({ webrtcUrl: BOT_URL }); + client.startUISnapshotStream(); + connectButton.dataset.state = "connected"; + connectButton.textContent = "Disconnect"; + connectButton.disabled = false; + setStatus("Connected. Try 'add milk and eggs'.", 5000); + } catch (err) { + console.error("Connect failed:", err); + setStatus(`Connect failed: ${err.message ?? err}`, 4000); + teardownUI(); + connectButton.disabled = false; + } +} + +async function disconnect() { + connectButton.disabled = true; + setStatus("Disconnecting…"); + try { + await client?.disconnect(); + } finally { + teardownUI(); + connectButton.dataset.state = ""; + connectButton.textContent = "Connect"; + connectButton.disabled = false; + } +} + +function teardownUI() { + client?.stopUISnapshotStream(); + unsubscribes.forEach((unsubscribe) => unsubscribe()); + unsubscribes = []; + if (botAudio.srcObject) botAudio.srcObject = null; + client = undefined; +} + +connectButton.addEventListener("click", () => { + if (connectButton.dataset.state === "connected") { + disconnect(); + } else { + connect(); + } +}); + +refreshEmptyState(); diff --git a/examples/multi-worker/ui-worker/shopping-list/client/package-lock.json b/examples/multi-worker/ui-worker/shopping-list/client/package-lock.json new file mode 100644 index 000000000..f9ea2ea9a --- /dev/null +++ b/examples/multi-worker/ui-worker/shopping-list/client/package-lock.json @@ -0,0 +1,1128 @@ +{ + "name": "shopping-list-client", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shopping-list-client", + "version": "0.1.0", + "dependencies": { + "@pipecat-ai/client-js": "1.9.0", + "@pipecat-ai/small-webrtc-transport": "^1.10.2" + }, + "devDependencies": { + "vite": "^8" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@daily-co/daily-js": { + "version": "0.90.0", + "resolved": "https://registry.npmjs.org/@daily-co/daily-js/-/daily-js-0.90.0.tgz", + "integrity": "sha512-XnuuNIxLt8GOv+rFYoU+OPTSmVj+Mg9hRPK2EM6WYVY62F6rcw7vwzZBOSCisVuu1VnTIF/2J9V0VEBa83lDzw==", + "license": "BSD-2-Clause", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@sentry/browser": "^8.33.1", + "bowser": "^2.8.1", + "dequal": "^2.0.3", + "events": "^3.1.0" + }, + "engines": { + "node": ">=22.14.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pipecat-ai/client-js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@pipecat-ai/client-js/-/client-js-1.9.0.tgz", + "integrity": "sha512-0pAmbKlYug7GL/8Zb8+KUB3KCNtnpwOl0MYhCWNiCfSIUSuQbDRbLBROPWVL8Aktj8PyANwQpPWQUq38m0bCag==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/events": "^3.0.3", + "bowser": "^2.11.0", + "clone-deep": "^4.0.1", + "events": "^3.3.0", + "typed-emitter": "^2.1.0", + "uuid": "^10.0.0" + } + }, + "node_modules/@pipecat-ai/small-webrtc-transport": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@pipecat-ai/small-webrtc-transport/-/small-webrtc-transport-1.10.2.tgz", + "integrity": "sha512-aGVWkg3XCaLOR0xr4G4YLpVR+P8kyitIMPPHLHfiQziuT6kme+4v0VZLUoIh9nbwE/PfIUpXNkhDqpAW+pulxg==", + "license": "BSD-2-Clause", + "dependencies": { + "@daily-co/daily-js": "^0.90.0", + "dequal": "^2.0.3", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@pipecat-ai/client-js": "~1.9.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.2.tgz", + "integrity": "sha512-GnKod+gL/Y+1FUM/RGV8q6le1CoyiGbT40MitEK7eVwWe+bfTRq1gN7ioupyHFMUg1RlQkDQ4/sENmio/uow5A==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.55.2.tgz", + "integrity": "sha512-XQy//NWbL0mLLM5w8wNDWMNpXz39VUyW2397dUrH8++kR63WhUVAvTOtL0o0GMVadSAzl1b08oHP9zSUNFQwcg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.55.2.tgz", + "integrity": "sha512-+W43Z697EVe/OgpGW07B773sa8xO1UbpnW0Cr+E+3FMDb6ZbXlaBUoagPTUkkQPdwBe35SDh6r8y2M3EOPGbxg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.2", + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.55.2.tgz", + "integrity": "sha512-P/jGiuR7dRLG9IzD/463fLgiibyYceauav/9prRG0ZxJm1AtuO02OKball2Fs3bbzdzwHCTlcsUuL2ivDF4b5A==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "8.55.2", + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.55.2.tgz", + "integrity": "sha512-xHuPIEKhx9zw5quWvv4YgZprnwoVMCfxIhmOIf6KJ9iizyUHeUDcKpLS59xERroqwX4RpvK+l/27AZu4zfZlzQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.2", + "@sentry-internal/feedback": "8.55.2", + "@sentry-internal/replay": "8.55.2", + "@sentry-internal/replay-canvas": "8.55.2", + "@sentry/core": "8.55.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.55.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.2.tgz", + "integrity": "sha512-YlEBwybUcOQ/KjMHDmof1vwweVnBtBxYlQp7DE3fOdtW4pqqdHWTnTntQs4VgYfxzjJYgtkd9LHlGtg8qy+JVQ==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/examples/multi-worker/ui-worker/shopping-list/client/package.json b/examples/multi-worker/ui-worker/shopping-list/client/package.json new file mode 100644 index 000000000..e2ce09512 --- /dev/null +++ b/examples/multi-worker/ui-worker/shopping-list/client/package.json @@ -0,0 +1,18 @@ +{ + "name": "shopping-list-client", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@pipecat-ai/client-js": "1.9.0", + "@pipecat-ai/small-webrtc-transport": "^1.10.2" + }, + "devDependencies": { + "vite": "^8" + } +} diff --git a/examples/multi-worker/ui-worker/shopping-list/client/styles.css b/examples/multi-worker/ui-worker/shopping-list/client/styles.css new file mode 100644 index 000000000..24de9435e --- /dev/null +++ b/examples/multi-worker/ui-worker/shopping-list/client/styles.css @@ -0,0 +1,216 @@ +:root { + --bg: #0f1115; + --panel: #181b22; + --panel-2: #1f242d; + --text: #e8ebf0; + --muted: #97a0ad; + --accent: #5b8cff; + --accent-soft: rgba(91, 140, 255, 0.18); + --done: #5fd08a; + --border: #2a2f3a; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: + system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + background: rgba(15, 17, 21, 0.92); + backdrop-filter: blur(6px); +} + +.topbar h1 { + font-size: 20px; + margin: 0; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 12px; +} + +#status { + font-size: 13px; + color: var(--muted); + transition: opacity 0.3s; +} + +#status[data-show="0"] { + opacity: 0; +} + +#connect { + font-size: 14px; + padding: 8px 18px; + border-radius: 8px; + border: 1px solid var(--accent); + background: var(--accent-soft); + color: var(--text); + cursor: pointer; +} + +#connect[data-state="connected"] { + border-color: var(--border); + background: transparent; + color: var(--muted); +} + +#connect:disabled { + opacity: 0.5; + cursor: default; +} + +main { + max-width: 560px; + margin: 0 auto; + padding: 28px 24px 80px; +} + +.hint { + color: var(--muted); + font-size: 14px; + margin: 0 0 18px; +} + +.add-form { + display: flex; + gap: 8px; + margin-bottom: 18px; +} + +.add-form input { + flex: 1; + font-size: 15px; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--panel); + color: var(--text); +} + +.add-form input:focus { + outline: none; + border-color: var(--accent); +} + +.add-form button { + font-size: 14px; + padding: 0 16px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--panel-2); + color: var(--text); + cursor: pointer; +} + +.list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.item { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 10px; + transition: + background 0.2s, + opacity 0.2s; +} + +.item label { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + cursor: pointer; +} + +.item input[type="checkbox"] { + width: 20px; + height: 20px; + accent-color: var(--done); + cursor: pointer; +} + +.item-text { + font-size: 16px; +} + +.item.checked { + opacity: 0.55; +} + +.item.checked .item-text { + text-decoration: line-through; + color: var(--muted); +} + +.list-empty { + color: var(--muted); + font-size: 14px; + text-align: center; + padding: 32px 0; +} + +.list-empty[hidden] { + display: none; +} + +/* New-item flash so a voice-added item is easy to spot. */ +.item-flash { + animation: item-flash 1s ease-out; +} + +@keyframes item-flash { + 0% { + background: var(--accent-soft); + border-color: var(--accent); + } + 100% { + background: var(--panel); + border-color: var(--border); + } +} + +/* Standard ``highlight`` command: a brief pulse. Duration is driven by + the server-supplied duration_ms via the --highlight-duration var. */ +.ui-highlight { + animation: ui-highlight var(--highlight-duration, 1500ms) ease-out; +} + +@keyframes ui-highlight { + 0% { + transform: scale(1); + box-shadow: 0 0 0 0 var(--accent-soft); + } + 30% { + transform: scale(1.02); + box-shadow: 0 0 0 6px var(--accent-soft); + border-color: var(--accent); + } + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(91, 140, 255, 0); + } +} diff --git a/examples/multi-worker/ui-worker/shopping-list/client/vite.config.js b/examples/multi-worker/ui-worker/shopping-list/client/vite.config.js new file mode 100644 index 000000000..ee3143e17 --- /dev/null +++ b/examples/multi-worker/ui-worker/shopping-list/client/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + server: { + port: 5173, + }, +}); \ No newline at end of file