From 07725429b2a8097f86c5f90d6043cdb4e3458975 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Thu, 21 May 2026 17:21:08 -0400 Subject: [PATCH] Add async-tasks UIWorker example A UIWorker with a custom reply tool fans research out to three BaseWorker peers via start_user_job_group; their progress streams to the client as ui-task cards and the user can cancel a group mid-flight. --- .../ui-worker/async-tasks/README.md | 90 ++ .../multi-worker/ui-worker/async-tasks/bot.py | 378 ++++++ .../ui-worker/async-tasks/client/index.html | 45 + .../ui-worker/async-tasks/client/main.js | 307 +++++ .../async-tasks/client/package-lock.json | 1128 +++++++++++++++++ .../ui-worker/async-tasks/client/package.json | 18 + .../ui-worker/async-tasks/client/styles.css | 245 ++++ .../async-tasks/client/vite.config.js | 7 + 8 files changed, 2218 insertions(+) create mode 100644 examples/multi-worker/ui-worker/async-tasks/README.md create mode 100644 examples/multi-worker/ui-worker/async-tasks/bot.py create mode 100644 examples/multi-worker/ui-worker/async-tasks/client/index.html create mode 100644 examples/multi-worker/ui-worker/async-tasks/client/main.js create mode 100644 examples/multi-worker/ui-worker/async-tasks/client/package-lock.json create mode 100644 examples/multi-worker/ui-worker/async-tasks/client/package.json create mode 100644 examples/multi-worker/ui-worker/async-tasks/client/styles.css create mode 100644 examples/multi-worker/ui-worker/async-tasks/client/vite.config.js diff --git a/examples/multi-worker/ui-worker/async-tasks/README.md b/examples/multi-worker/ui-worker/async-tasks/README.md new file mode 100644 index 000000000..2ace60701 --- /dev/null +++ b/examples/multi-worker/ui-worker/async-tasks/README.md @@ -0,0 +1,90 @@ +# async-tasks + +The UIWorker fans out long-running work to multiple peer workers in +parallel, streams their progress to an in-flight panel on the page, and +lets the user cancel mid-flight. + +## What it shows + +- The **`user_job_group` / `start_user_job_group`** API: dispatching + parallel work to multiple peer workers and automatically forwarding + every job lifecycle event to the client. The `reply` tool calls + `start_user_job_group("wikipedia", "news", "scholar", payload=..., + label=...)` and the `UIWorker` does the rest. +- The four **`ui-task` envelopes** the worker forwards (`group_started`, + `task_update`, `task_completed`, `group_completed`) and the + client-side `RTVIEvent.UITask` event for consuming them. The client + keeps a state map keyed by `task_id` and renders per-worker progress. +- **Cancellation**: the in-flight card's Cancel button calls + `client.cancelUITask(task_id, reason)`. The reserved `__cancel_task` + event is translated by the `UIWorker` into `cancel_job_group(task_id)` + on the registered group; cancelled workers report status `cancelled`. +- **Background dispatch from a tool**: `start_user_job_group` returns + immediately so the `reply` tool can speak its acknowledgement + ("Researching the Mariana Trench now") while the workers run — the + main LLM is free to take follow-up turns. + +## What it adds vs. the prior demos + +The other examples use the request/response half of the bus protocol +(main LLM → UIWorker → reply). This one adds the streaming job-group +half: UIWorker → peer workers → progress events forwarded to the client. +The architecture grows from "one delegate" to "one delegate plus a +worker pool" — the peers are plain `BaseWorker`s launched on the runner. + +## Run + +Two terminals. + +**Terminal 1 — bot:** + +```bash +cd examples/multi-worker/ui-worker/async-tasks +uv run python bot.py +``` + +The bot starts on `http://localhost:7860`. + +**Terminal 2 — client:** + +```bash +cd examples/multi-worker/ui-worker/async-tasks/client +npm install # one-time +npm run dev +``` + +Open `http://localhost:5173` and click **Connect**. + +## What to try + +The workers are simulated (canned summaries, randomized `asyncio.sleep` +delays) so the demo focuses on the protocol, not the AI. Each research +call takes a few seconds. + +- _"Research the Mariana Trench."_ — the worker spawns three peers, + acknowledges in one short reply, and a card appears showing each + peer's status as it progresses (searching → found N results → + summarizing → completed). +- _"Look up octopus cognition."_ — same flow; a second card stacks. +- _"Research the moon, then research Mars."_ — two groups run + concurrently. +- _"How are you?"_ (no research) — quick reply, no job group. +- **Click Cancel on an in-flight card** — the cancellation routes + through, the peers' tasks raise `CancelledError`, and their responses + come back as `cancelled`. + +## 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 + +Real worker integrations (the peers are simulated), LLM-driven peers +(these are pure data-fetch — a peer can itself be an `LLMWorker`), +streaming chunks (`send_job_stream_data` for progressive output), or +worker-to-worker fan-out (nested job groups). diff --git a/examples/multi-worker/ui-worker/async-tasks/bot.py b/examples/multi-worker/ui-worker/async-tasks/bot.py new file mode 100644 index 000000000..dc71113bc --- /dev/null +++ b/examples/multi-worker/ui-worker/async-tasks/bot.py @@ -0,0 +1,378 @@ +# +# Copyright (c) 2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Async tasks — the UIWorker fans out long-running work and streams progress. + +The user asks the assistant to research a topic. The UIWorker dispatches +three peer workers (Wikipedia, news, scholarly papers) in parallel via +``start_user_job_group``. Each worker emits progress updates while it +works. ``UIWorker`` forwards every lifecycle event to the client as +``ui-task`` envelopes (``group_started``, ``task_update``, +``task_completed``, ``group_completed``), which the client renders as +in-flight cards with per-worker status. The user can cancel a group +mid-flight via ``client.cancelUITask(task_id)``, which sends a reserved +``__cancel_task`` event that the worker turns into a ``cancel_job_group`` +call. + +Architecture:: + + Main worker (PipelineWorker, owns transport + RTVI): + transport.in → STT → user_agg → LLM → TTS → transport.out → assistant_agg + └── answer_about_screen(query) tool + └── params.pipeline_worker.job("ui", name="respond", payload={query}) + + ResearchWorker (UIWorker): + └── @tool reply(answer, research_query=None) + └── (if research_query) start_user_job_group("wikipedia", "news", "scholar") + + Three peer workers (BaseWorker each): + WikipediaResearcher · NewsResearcher · ScholarResearcher + +The workers are deliberately simulated with ``asyncio.sleep`` and canned +summaries so the demo focuses on the protocol, not the AI. A real app +would wire each worker to its own data source. + +``start_user_job_group`` dispatches the group on a background task and +returns immediately, so the spoken "researching X" acknowledgement frees +the main LLM to take new turns while the workers continue. + +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 asyncio +import os +import random + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.bus.messages import BusJobRequestMessage +from pipecat.frames.frames import LLMRunFrame +from pipecat.pipeline.base_worker import BaseWorker +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" + +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 layer of a research assistant. A separate UI \ +layer sees the page and dispatches research tasks. + +For every user utterance involving research (asking about a topic, \ +launching a search, asking for follow-ups), call \ +``answer_about_screen`` with the user's request verbatim. The \ +tool's response is the spoken reply, already TTS-ready. + +Only respond directly for pure pleasantries (greetings, thanks, \ +goodbyes). Keep direct replies to one short spoken sentence.""" + + +# 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 help the user research topics. When the user names something \ +to look up, kick off a parallel research task across three worker \ +sources (Wikipedia, news, scholarly papers). + +## Tool: reply + +Every turn calls ``reply`` exactly once. One tool call per turn. + +``reply(answer, research_query=None)``: + +- ``answer`` (REQUIRED): the spoken reply, plain language, one \ +short sentence. No markdown, no symbols. +- ``research_query`` (OPTIONAL): the topic to research. When set, \ +the server fans out three worker agents in parallel and streams \ +their progress to an in-flight panel on the page. The workers run \ +in the background; you do NOT wait for results. Just speak a brief \ +acknowledgement. + +## Decision rules + +- **User asks to research / look up / find out about something** → \ +set ``research_query`` to the topic and answer with a brief \ +acknowledgement ("Researching the Mariana Trench now"). The server \ +handles the rest; results stream onto the page. +- **User asks a quick question you can answer immediately** → just \ +``answer``. Don't kick off a research task for trivia or for \ +questions about the in-flight tasks themselves. +- **User asks about ongoing research** → just ``answer`` (the \ +results panel on screen shows progress). + +## Examples + +- "Research the Mariana Trench." → \ +``reply(answer="Researching the Mariana Trench now.", research_query="Mariana Trench")`` +- "Look up octopus cognition." → \ +``reply(answer="Looking that up.", research_query="octopus cognition")`` +- "How many neurons does an octopus have?" (quick question, no \ +research needed) → ``reply(answer="About five hundred million.")`` +- "Hi." → ``reply(answer="Hi! What would you like to research?")``""" + + +class _SimulatedResearcher(BaseWorker): + """BaseWorker peer that fakes a research task with progress updates. + + Receives a ``payload={"query": ...}``. Emits a few ``send_job_update`` + messages with progress text, then a final ``send_job_response`` + carrying a canned summary. The randomized ``asyncio.sleep`` makes the + workers feel like they run at different paces, which shows off the + streaming UI. + + Subclasses set ``source_name`` and provide ``summarize(query)``. + """ + + source_name: str = "researcher" + + def summarize(self, query: str) -> str: + return f"Generic results for '{query}'." + + async def on_job_request(self, message: BusJobRequestMessage) -> None: + await super().on_job_request(message) + job_id = message.job_id + query = (message.payload or {}).get("query", "") + try: + await asyncio.sleep(random.uniform(0.4, 1.2)) + await self.send_job_update(job_id, {"text": f"searching {self.source_name}…"}) + + await asyncio.sleep(random.uniform(0.6, 1.4)) + n = random.randint(3, 8) + await self.send_job_update(job_id, {"text": f"found {n} results"}) + + await asyncio.sleep(random.uniform(0.5, 1.5)) + await self.send_job_update(job_id, {"text": "summarizing"}) + + await asyncio.sleep(random.uniform(0.4, 0.9)) + await self.send_job_response(job_id, response={"summary": self.summarize(query)}) + except asyncio.CancelledError: + # The base worker's cancellation hook auto-emits a CANCELLED + # response; just bail. + raise + + +class WikipediaResearcher(_SimulatedResearcher): + source_name = "wikipedia" + + def summarize(self, query: str) -> str: + return ( + f"Wikipedia overview of {query}: a one-paragraph summary covering " + "the historical background, key facts, and major figures." + ) + + +class NewsResearcher(_SimulatedResearcher): + source_name = "news" + + def summarize(self, query: str) -> str: + return ( + f"Recent news on {query}: three headlines from the past month, " + "a short context paragraph, and any active developments." + ) + + +class ScholarResearcher(_SimulatedResearcher): + source_name = "scholar" + + def summarize(self, query: str) -> str: + return ( + f"Scholarly take on {query}: two highly cited papers, the " + "consensus position, and a notable debate or open question." + ) + + +class ResearchWorker(UIWorker): + """UIWorker that kicks off background research job groups. + + The custom ``@tool reply`` has a ``research_query`` field. When the + LLM sets it, the tool fires ``start_user_job_group(...)`` against the + three peer workers — fire-and-forget from the LLM's perspective, so + the tool returns immediately with the spoken acknowledgement. The + ``UIWorker`` forwards every job lifecycle event to the client as + ``ui-task`` envelopes, where the client renders progress and a cancel + button. + """ + + def __init__(self): + llm = OpenAILLMService( + api_key=os.environ["OPENAI_API_KEY"], + settings=OpenAILLMService.Settings(system_instruction=UI_PROMPT), + ) + super().__init__("ui", llm=llm) + + @tool + async def reply( + self, + params: FunctionCallParams, + answer: str, + research_query: str | None = None, + ): + """Reply to the user. Optionally kick off background research. + + Always called exactly once per turn. ``answer`` is required. + + Args: + answer: The spoken reply in plain language. One short + sentence. For research turns, a brief acknowledgement + like "Researching X now." + research_query: Optional topic to research. When set, the + server fans out three worker agents in parallel and + streams progress to the page. Workers run in the + background; the LLM does NOT wait for results. + """ + logger.info(f"{self}: reply(answer={answer!r}, research_query={research_query!r})") + if research_query: + await self.start_user_job_group( + "wikipedia", + "news", + "scholar", + payload={"query": research_query}, + label=f"Research: {research_query}", + ) + await self.respond_to_job(speak=answer) + await params.result_callback(None) + + +async def answer_about_screen(params: FunctionCallParams, query: str): + """Forward the user's request to the screen-aware research worker. + + Args: + query (str): The user's request, passed verbatim. + """ + logger.info(f"answer_about_screen('{query}')") + try: + async with params.pipeline_worker.job( + "ui", name="respond", payload={"query": query}, timeout=10 + ) as t: + pass + except JobError as e: + logger.warning(f"ui job failed: {e}") + await params.result_callback("Something went wrong on my side.") + return + + speak = (t.response or {}).get("speak") + await params.result_callback(speak or "I'm not sure how to answer that.") + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info("Starting async-tasks 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), + ) + llm.register_direct_function(answer_about_screen, cancel_on_interruption=False, timeout_secs=30) + + context = LLMContext(tools=ToolsSchema(standard_tools=[answer_about_screen])) + aggregators = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), + stt, + aggregators.user(), + llm, + tts, + transport.output(), + aggregators.assistant(), + ] + ) + + worker = PipelineWorker( + pipeline, + name=MAIN_NAME, + params=PipelineParams(enable_metrics=True, enable_usage_metrics=True), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @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 ask you to " + "research any topic. 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(ResearchWorker()) + await runner.launch_worker(WikipediaResearcher("wikipedia")) + await runner.launch_worker(NewsResearcher("news")) + await runner.launch_worker(ScholarResearcher("scholar")) + 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/async-tasks/client/index.html b/examples/multi-worker/ui-worker/async-tasks/client/index.html new file mode 100644 index 000000000..a38b82699 --- /dev/null +++ b/examples/multi-worker/ui-worker/async-tasks/client/index.html @@ -0,0 +1,45 @@ + + + + + + Async tasks — UIAgent demo + + + +
+

Research lab

+ +
+ +
+

+ Try: "research the Mariana Trench" — the agent fans out three + worker researchers (Wikipedia, news, scholarly papers) in + parallel. Each streams progress updates while it works. You + can cancel any group mid-flight. +

+ +
+

In flight

+
+ No tasks yet. Ask the assistant to research something. +
+
+
+ +
+

Results

+
+ Completed research will appear here. +
+
+
+
+ +
+ + + + + diff --git a/examples/multi-worker/ui-worker/async-tasks/client/main.js b/examples/multi-worker/ui-worker/async-tasks/client/main.js new file mode 100644 index 000000000..d3426eace --- /dev/null +++ b/examples/multi-worker/ui-worker/async-tasks/client/main.js @@ -0,0 +1,307 @@ +/** + * Async tasks — vanilla JS client. + * + * Same base wiring as the other examples (PipecatClient + + * managed snapshot streaming + bot audio sink), with one new piece: + * ``RTVIEvent.UITask`` subscription to consume the task lifecycle + * envelopes. + * + * The server's ``user_task_group`` fans work out to multiple + * worker agents and forwards their progress automatically as + * ``ui-task`` envelopes. Four kinds: + * + * - ``group_started``: workers and label are now known. + * - ``task_update``: a worker emitted a progress update. + * - ``task_completed``: a worker finished (status + final response). + * - ``group_completed``: every worker has responded. + * + * The client maintains a state map keyed by ``task_id``, renders + * each group as a card with its workers' statuses, and surfaces a + * cancel button per cancellable group. ``client.cancelUITask(task_id, + * reason)`` sends a ``__cancel_task`` event back to the server, + * which calls ``UIAgent.cancel_task(...)`` on the registered group. + */ + +import { PipecatClient, RTVIEvent } 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 tasksList = document.getElementById("tasks-list"); +const tasksEmpty = document.getElementById("tasks-empty"); +const resultsList = document.getElementById("results-list"); +const resultsEmpty = document.getElementById("results-empty"); + +let client; +let unsubscribeTasks; + +// Map, cardEl }> +const groups = new Map(); + +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 refreshEmptyStates() { + tasksEmpty.hidden = tasksList.children.length > 0; + resultsEmpty.hidden = resultsList.children.length > 0; +} + +function renderGroupCard(group) { + const card = document.createElement("div"); + card.className = "task-group"; + card.dataset.taskId = group.task_id; + + const header = document.createElement("div"); + header.className = "task-group-header"; + const label = document.createElement("div"); + label.className = "task-group-label"; + label.textContent = group.label ?? `Task group ${group.task_id.slice(0, 8)}`; + header.appendChild(label); + + if (group.cancellable) { + const cancel = document.createElement("button"); + cancel.type = "button"; + cancel.className = "cancel-btn"; + cancel.textContent = "Cancel"; + cancel.addEventListener("click", () => { + cancel.disabled = true; + cancel.textContent = "Cancelling…"; + client?.cancelUITask(group.task_id, "user requested"); + }); + group.cancelButton = cancel; + header.appendChild(cancel); + } + card.appendChild(header); + + const ul = document.createElement("ul"); + ul.className = "workers"; + for (const agent of group.agents) { + const li = document.createElement("li"); + li.dataset.agent = agent; + + const name = document.createElement("span"); + name.className = "worker-name"; + name.textContent = agent; + li.appendChild(name); + + const update = document.createElement("span"); + update.className = "worker-update"; + update.textContent = "starting…"; + li.appendChild(update); + + const stat = document.createElement("span"); + stat.className = "worker-status"; + stat.dataset.status = "running"; + stat.textContent = "running"; + li.appendChild(stat); + + ul.appendChild(li); + } + card.appendChild(ul); + + group.cardEl = card; + group.listEl = ul; + return card; +} + +function updateWorkerRow(group, agentName, { update, statusValue, response }) { + const li = group.listEl.querySelector(`li[data-agent="${CSS.escape(agentName)}"]`); + if (!li) return; + if (update !== undefined) { + li.querySelector(".worker-update").textContent = update; + } + if (statusValue !== undefined) { + const stat = li.querySelector(".worker-status"); + stat.dataset.status = statusValue; + stat.textContent = statusValue; + if (statusValue !== "running" && response !== undefined) { + // Tuck the response into the row so we can lift it into the + // results panel when the group completes. + li.dataset.response = JSON.stringify(response); + } + } +} + +function renderResultsForGroup(group) { + const card = document.createElement("div"); + card.className = "result-card"; + + const label = document.createElement("div"); + label.className = "result-card-label"; + label.textContent = group.label ?? "Result"; + card.appendChild(label); + + const meta = document.createElement("div"); + meta.className = "result-card-meta"; + const counts = { completed: 0, cancelled: 0, failed: 0, error: 0 }; + group.workers.forEach((w) => { + if (w.status in counts) counts[w.status] += 1; + }); + const parts = []; + if (counts.completed) parts.push(`${counts.completed} completed`); + if (counts.cancelled) parts.push(`${counts.cancelled} cancelled`); + if (counts.failed) parts.push(`${counts.failed} failed`); + if (counts.error) parts.push(`${counts.error} error`); + meta.textContent = parts.join(" · ") || "no workers"; + card.appendChild(meta); + + group.workers.forEach((w, agent) => { + if (w.status !== "completed") return; + const section = document.createElement("div"); + section.className = "result-card-section"; + const src = document.createElement("span"); + src.className = "source"; + src.textContent = agent + ": "; + section.appendChild(src); + const summary = + w.response?.summary ?? w.response?.text ?? JSON.stringify(w.response); + section.appendChild(document.createTextNode(summary)); + card.appendChild(section); + }); + + return card; +} + +function handleTaskEnvelope(env) { + switch (env.kind) { + case "group_started": { + const workers = new Map(); + for (const a of env.agents) { + workers.set(a, { status: "running", update: null, response: null }); + } + const group = { + task_id: env.task_id, + label: env.label, + cancellable: env.cancellable, + agents: env.agents, + workers, + }; + groups.set(env.task_id, group); + tasksList.appendChild(renderGroupCard(group)); + refreshEmptyStates(); + break; + } + case "task_update": { + const group = groups.get(env.task_id); + if (!group) break; + const text = env.data?.text ?? JSON.stringify(env.data); + const w = group.workers.get(env.agent_name); + if (w) w.update = text; + updateWorkerRow(group, env.agent_name, { update: text }); + break; + } + case "task_completed": { + const group = groups.get(env.task_id); + if (!group) break; + const w = group.workers.get(env.agent_name); + if (w) { + w.status = env.status; + w.response = env.response; + } + const display = env.response?.summary + ? env.response.summary.slice(0, 60) + "…" + : env.status; + updateWorkerRow(group, env.agent_name, { + update: display, + statusValue: env.status, + response: env.response, + }); + break; + } + case "group_completed": { + const group = groups.get(env.task_id); + if (!group) break; + // Lift the in-flight card into the results panel, then drop + // the in-flight card. + resultsList.prepend(renderResultsForGroup(group)); + group.cardEl.remove(); + groups.delete(env.task_id); + refreshEmptyStates(); + break; + } + } +} + +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]); + }); + + client.on(RTVIEvent.UITask, handleTaskEnvelope); + unsubscribeTasks = () => client.off(RTVIEvent.UITask, handleTaskEnvelope); + + try { + await client.connect({ webrtcUrl: BOT_URL }); + client.startUISnapshotStream(); + connectButton.dataset.state = "connected"; + connectButton.textContent = "Disconnect"; + connectButton.disabled = false; + setStatus("Connected. Try: 'research the Mariana Trench'", 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(); + unsubscribeTasks?.(); + if (botAudio.srcObject) botAudio.srcObject = null; + unsubscribeTasks = undefined; + client = undefined; +} + +connectButton.addEventListener("click", () => { + if (connectButton.dataset.state === "connected") { + disconnect(); + } else { + connect(); + } +}); + +refreshEmptyStates(); diff --git a/examples/multi-worker/ui-worker/async-tasks/client/package-lock.json b/examples/multi-worker/ui-worker/async-tasks/client/package-lock.json new file mode 100644 index 000000000..72f1cfddd --- /dev/null +++ b/examples/multi-worker/ui-worker/async-tasks/client/package-lock.json @@ -0,0 +1,1128 @@ +{ + "name": "async-tasks-client", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "async-tasks-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/async-tasks/client/package.json b/examples/multi-worker/ui-worker/async-tasks/client/package.json new file mode 100644 index 000000000..c2ed43ac2 --- /dev/null +++ b/examples/multi-worker/ui-worker/async-tasks/client/package.json @@ -0,0 +1,18 @@ +{ + "name": "async-tasks-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/async-tasks/client/styles.css b/examples/multi-worker/ui-worker/async-tasks/client/styles.css new file mode 100644 index 000000000..1f5cacf56 --- /dev/null +++ b/examples/multi-worker/ui-worker/async-tasks/client/styles.css @@ -0,0 +1,245 @@ +:root { + color-scheme: light; + font-family: system-ui, -apple-system, sans-serif; + --border: #d4d4d8; + --muted: #71717a; + --accent: #3b82f6; + --success: #16a34a; + --error: #dc2626; + --cancelled: #71717a; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: #fafafa; + color: #18181b; +} + +header { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border); + background: #fff; +} + +header h1 { + font-size: 1.125rem; + margin: 0; +} + +#connect { + padding: 0.5rem 1rem; + border: 1px solid var(--border); + background: #fff; + border-radius: 6px; + cursor: pointer; + font-size: 0.875rem; +} + +#connect:hover { + background: #f4f4f5; +} + +#connect[data-state="connected"] { + background: #ef4444; + color: white; + border-color: #ef4444; +} + +main { + max-width: 720px; + margin: 0 auto; + padding: 1.5rem 1.5rem 4rem; +} + +.hint { + margin: 0 0 1.5rem; + font-size: 0.9375rem; + line-height: 1.5; + color: var(--muted); + padding: 0.875rem 1rem; + border-left: 3px solid var(--border); + background: #fff; +} + +main h2 { + font-size: 0.8125rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--muted); + margin: 1.5rem 0 0.75rem; +} + +.empty-state { + font-size: 0.875rem; + color: var(--muted); + font-style: italic; + padding: 1rem; + border: 1px dashed var(--border); + border-radius: 8px; + text-align: center; +} + +.empty-state[hidden] { + display: none; +} + +.task-group { + background: #fff; + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem 1.25rem; + margin-bottom: 0.75rem; +} + +.task-group-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.5rem; +} + +.task-group-label { + font-weight: 500; + font-size: 0.9375rem; +} + +.cancel-btn { + padding: 0.25rem 0.625rem; + border: 1px solid var(--border); + background: #fff; + border-radius: 4px; + cursor: pointer; + font-size: 0.75rem; + color: var(--muted); +} + +.cancel-btn:hover { + background: #f4f4f5; + color: #18181b; +} + +.cancel-btn[disabled] { + opacity: 0.4; + cursor: not-allowed; +} + +.workers { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.workers li { + display: flex; + align-items: baseline; + gap: 0.5rem; + font-size: 0.875rem; +} + +.worker-name { + font-family: ui-monospace, "SF Mono", Menlo, monospace; + font-size: 0.8125rem; + font-weight: 500; + min-width: 6.5rem; + color: #52525b; +} + +.worker-update { + color: #3f3f46; + flex: 1; + font-style: italic; +} + +.worker-status { + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.worker-status[data-status="running"] { + color: var(--accent); +} + +.worker-status[data-status="completed"] { + color: var(--success); +} + +.worker-status[data-status="cancelled"] { + color: var(--cancelled); +} + +.worker-status[data-status="failed"], +.worker-status[data-status="error"] { + color: var(--error); +} + +.result-card { + background: #fff; + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem 1.25rem; + margin-bottom: 0.75rem; +} + +.result-card-label { + font-weight: 500; + font-size: 0.9375rem; + margin-bottom: 0.375rem; +} + +.result-card-meta { + font-size: 0.75rem; + color: var(--muted); + margin-bottom: 0.625rem; +} + +.result-card-section { + font-size: 0.875rem; + line-height: 1.5; + margin-bottom: 0.5rem; +} + +.result-card-section:last-child { + margin-bottom: 0; +} + +.result-card-section .source { + font-family: ui-monospace, "SF Mono", Menlo, monospace; + font-size: 0.75rem; + font-weight: 500; + color: #52525b; +} + +#status { + position: fixed; + bottom: 1rem; + right: 1rem; + padding: 0.5rem 0.75rem; + border-radius: 6px; + font-size: 0.8125rem; + background: #18181b; + color: white; + opacity: 0; + transition: opacity 0.2s; + pointer-events: none; +} + +#status[data-show="1"] { + opacity: 1; +} diff --git a/examples/multi-worker/ui-worker/async-tasks/client/vite.config.js b/examples/multi-worker/ui-worker/async-tasks/client/vite.config.js new file mode 100644 index 000000000..99a600b50 --- /dev/null +++ b/examples/multi-worker/ui-worker/async-tasks/client/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + server: { + port: 5173, + }, +});