diff --git a/moss-live-labs/examples/voice-agent/.env.example b/moss-live-labs/examples/voice-agent/.env.example index 0ed7ca62..2722ed67 100644 --- a/moss-live-labs/examples/voice-agent/.env.example +++ b/moss-live-labs/examples/voice-agent/.env.example @@ -7,7 +7,10 @@ LIVEKIT_API_SECRET=secret MOSS_PROJECT_ID=Your moss project id here MOSS_PROJECT_KEY=Your moss project key here -# AI Provider Keys +# Initial region (US or EU) — the UI region picker overrides this live per query. +MOSS_REGION=US + +# AI Provider Keys (OpenAI = LLM, Deepgram = STT, Cartesia = TTS) OPENAI_API_KEY=Your openai api key here DEEPGRAM_API_KEY=Your deepgram api key here CARTESIA_API_KEY=Your cartesia api key here \ No newline at end of file diff --git a/moss-live-labs/examples/voice-agent/DEMO_SCRIPT_METADATA.md b/moss-live-labs/examples/voice-agent/DEMO_SCRIPT_METADATA.md new file mode 100644 index 00000000..b286e05e --- /dev/null +++ b/moss-live-labs/examples/voice-agent/DEMO_SCRIPT_METADATA.md @@ -0,0 +1,49 @@ +# Demo Script — Metadata Filtering (region-scoped answers) + +~90s. Shows the **same question** returning **different, correct answers** by region, +because Moss filters retrieval on `metadata.region` — one shared index, scoped per line. + +## Before you record +- [ ] **Re-seed** (metadata changed): delete the old `demo-customer_faqs` index in the Moss + portal, then `python seed_index.py`. +- [ ] `python agent.py dev` · `livekit-server --dev` · web UI at localhost:3000 +- [ ] Region is chosen with the **US / EU picker** in the panel (starts on US) — no restart. + +## 1 · Frame it (0:00–0:12) +> "Same knowledge base, one index. This support panel has a region picker — right now it's +> set to the US, and Moss only retrieves policies that apply to that region. Watch." + +**[Connect. Panel shows the picker on US and `filter · region ∈ [US, all]`.]** + +## 2 · US answer (0:12–0:35) +> "What's your return window?" + +**[Point at panel: the filter line + the retrieved US chunk.]** +> "Thirty days. The panel shows the filter — region is US or global — and it pulled the +> US returns policy. The EU policy is in the same index, but it was filtered out." + +## 3 · Switch region (0:35–1:05) +**[Click EU in the picker — the filter line flips to `region ∈ [EU, all]` instantly.]** +> "Now I switch the same agent to the EU — no restart, just the filter. Same question." + +> "What's your return window?" + +> "Fourteen days — the EU right of withdrawal. Same index, same question, different answer, +> because the metadata filter scoped retrieval to the right region." + +## 4 · Why it matters (1:05–1:25) +> "That's one line of filter in the query. It's how you serve region-specific policies, +> or isolate tenants, or gate content by plan — all from a single Moss index, evaluated +> locally in milliseconds. Open source at github.com/usemoss/moss." + +--- + +## The filter (for reference) +```python +QueryOptions(top_k=5, alpha=0.8, + filter={"field": "region", "condition": {"$in": ["EU", "all"]}}) +``` + +## Try these too +- "How much is shipping?" → US quotes dollars, EU quotes euros (with VAT). +- A global question ("I forgot my password") → same answer in both regions (region = `all`). diff --git a/moss-live-labs/examples/voice-agent/README.md b/moss-live-labs/examples/voice-agent/README.md index 3130a67d..fd34632e 100644 --- a/moss-live-labs/examples/voice-agent/README.md +++ b/moss-live-labs/examples/voice-agent/README.md @@ -1,58 +1,73 @@ # Voice Agent -A voice assistant that answers questions using your Moss knowledge base. You talk to it, it searches your data, and responds out loud. +A customer-support **voice agent** grounded in a Moss knowledge base, with a brand-styled +web UI that shows Moss doing the retrieval live. You talk to it, it searches your data, +and a side panel surfaces the exact chunks Moss returned (with match scores and query +latency) for every turn. + +``` +Browser (web/) ⟷ LiveKit room ⟷ agent.py (STT → LLM → TTS) ⟷ Moss (RAG) +``` ## What you need -- A [Moss](https://moss.dev) account with a project and an index loaded with your data -- A [LiveKit](https://livekit.io) account (or run it locally) -- An [OpenAI](https://platform.openai.com) API key (for the AI responses) -- A [Deepgram](https://deepgram.com) API key (for speech-to-text) -- A [Cartesia](https://play.cartesia.ai) API key (for text-to-speech) +- A [Moss](https://moss.dev) account (project ID + key) +- [LiveKit](https://livekit.io) running locally (`livekit-server --dev`) +- [OpenAI](https://platform.openai.com) (LLM), [Deepgram](https://deepgram.com) + (speech-to-text), and [Cartesia](https://cartesia.ai) (text-to-speech) API keys +- Python 3.14+ (`uv`) and Node 18.18+ (`npm`) for the web UI ## Setup -1. Install dependencies: +```bash +uv sync +cp .env.example .env # fill in your Moss + provider keys +python agent.py download-files +``` - ```bash - uv sync - ``` +`.env` keys: `MOSS_PROJECT_ID`, `MOSS_PROJECT_KEY`, `OPENAI_API_KEY`, `DEEPGRAM_API_KEY`, `CARTESIA_API_KEY`. +For local LiveKit, leave the `LIVEKIT_*` values as-is; for LiveKit Cloud, set them to your +project's URL/key/secret and copy the same three into `web/.env.local`. The index name +defaults to `demo-customer_faqs` (override with `MOSS_INDEX_NAME`). -2. Copy the env file and fill in your keys: +## 1. Seed the knowledge base - ```bash - cp .env.example .env - ``` +Loads the sample support FAQs in `data/faqs.json` into a Moss index: - Open `.env` and add: +```bash +python seed_index.py +``` - ```env - LIVEKIT_URL=ws://localhost:7880 - LIVEKIT_API_KEY=devkey - LIVEKIT_API_SECRET=secret +## 2. Run it - MOSS_PROJECT_ID=your-project-id - MOSS_PROJECT_KEY=your-project-key - MOSS_INDEX_NAME=your-index-name +Open three terminals: - OPENAI_API_KEY=... - DEEPGRAM_API_KEY=... - CARTESIA_API_KEY=... - ``` +```bash +# a) LiveKit server (local dev — uses devkey/secret) +livekit-server --dev -3. Download the required model files: +# b) the agent (joins rooms automatically) +python agent.py dev - ```bash - python agent.py download-files - ``` +# c) the web UI +cd web +npm install +cp .env.local.example .env.local +npm run dev # → http://localhost:3000 +``` -## Run +Open http://localhost:3000, click **Start the demo**, and talk. The right-hand panel +shows what Moss retrieves on each turn. -```bash -python agent.py console -``` +> Prefer no UI? `python agent.py console` still works for a mic-only, terminal session. + +## How the retrieval panel works + +On each user turn, `agent.py` queries Moss and publishes the results to the LiveKit room +on the `moss.retrieval` data channel (`{query, docs:[{text, score}], took_ms}`). The web +UI listens on that channel and renders them. The voice pipeline is otherwise untouched. -Speak into your microphone and the agent responds out loud. +See [`DEMO_SCRIPT_METADATA.md`](./DEMO_SCRIPT_METADATA.md) for a ready-to-record walkthrough. ## Resources diff --git a/moss-live-labs/examples/voice-agent/agent.py b/moss-live-labs/examples/voice-agent/agent.py index d85b6606..a4841cca 100644 --- a/moss-live-labs/examples/voice-agent/agent.py +++ b/moss-live-labs/examples/voice-agent/agent.py @@ -1,7 +1,11 @@ +import json import logging import os +import time from dotenv import load_dotenv +from livekit import rtc from livekit.plugins import openai, deepgram, silero, cartesia +from livekit.plugins.turn_detector.multilingual import MultilingualModel from livekit.agents import ( JobContext, WorkerOptions, @@ -22,49 +26,110 @@ MOSS_PROJECT_ID = os.getenv("MOSS_PROJECT_ID") MOSS_PROJECT_KEY = os.getenv("MOSS_PROJECT_KEY") INDEX_NAME = os.getenv("MOSS_INDEX_NAME", "demo-customer_faqs") +# This support line serves one region. Metadata filtering scopes retrieval to +# region-specific policies + global ("all") docs. Set MOSS_REGION=EU to compare. +ALLOWED_REGIONS = {"US", "EU"} +REGION = os.getenv("MOSS_REGION", "US") +if REGION not in ALLOWED_REGIONS: + REGION = "US" logging.basicConfig(level=logging.INFO) logger = logging.getLogger("moss-agent") class MossSemanticRetrievalAgent(Agent): - def __init__(self, moss_client: MossClient): + def __init__(self, moss_client: MossClient, room: rtc.Room): super().__init__( instructions=""" - You are a helpful customer support voice assistant. - You have access to a knowledge base which will be provided to you as context. - Always answer the user's question based on the provided context. - If the context doesn't contain the answer, politely say you don't know. + You are Northwind's customer support voice assistant, speaking directly to + one customer on a call. Answer naturally and concisely using ONLY the + knowledge-base context provided for the current question. + + - Present policies as simply "our policy" — the customer's own. Never mention + regions, "other regions", that policies vary by location, the knowledge + base, filters, or how you look answers up. + - The context can change between questions; do not reuse facts or numbers from + earlier in the conversation if they are not in the current context. + - If the current context doesn't answer the question, say you don't know and + offer to help with something else. + + Keep replies to a sentence or two, warm and clear for voice. """ ) self.moss = moss_client + self.room = room + self.region = REGION # live-updated from the UI region picker + + async def _publish_retrieval(self, query: str, results, fallback_ms: float) -> None: + """Send the retrieved chunks to the web UI over a LiveKit data channel.""" + # Use Moss's own server-reported search time; fall back to wall-clock. + server_ms = getattr(results, "time_taken_ms", None) + took_ms = float(server_ms) if server_ms is not None else fallback_ms + payload = { + "query": query, + "docs": [ + { + "id": getattr(d, "id", None), + "text": d.text, + "score": float(getattr(d, "score", 0.0)), + } + for d in (results.docs if results and results.docs else []) + ], + "took_ms": round(took_ms, 2), + "region": self.region, + } + try: + await self.room.local_participant.publish_data( + payload=json.dumps(payload).encode("utf-8"), + reliable=True, + topic="moss.retrieval", + ) + except Exception as e: + logger.warning(f"Failed to publish retrieval data: {e}") async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatMessage) -> None: """ Intercept user message -> Search Moss -> Inject Context -> Continue """ user_query = new_message.text_content + if not user_query or not user_query.strip(): + # ignore empty/interim transcription artifacts + await super().on_user_turn_completed(turn_ctx, new_message) + return logger.info(f"User asked: {user_query}") try: - # 1. Automatic Search + # 1. Automatic Search — metadata-filtered to this region + global docs + region_filter = {"field": "region", "condition": {"$in": [self.region, "all"]}} + t0 = time.perf_counter() results = await self.moss.query( INDEX_NAME, user_query, - QueryOptions(top_k=5, alpha=0.8) + QueryOptions(top_k=5, alpha=0.8, filter=region_filter), ) - - # 2. Context Injection + took_ms = (time.perf_counter() - t0) * 1000.0 + + # 2. Stream the retrieval to the web UI (the Moss knowledge-base panel) + await self._publish_retrieval(user_query, results, took_ms) + + # 3. Context Injection if results.docs: context_str = "\n".join([f"- {d.text}" for d in results.docs]) - injection = f"Relevant context from knowledge base:\n{context_str}\n\nUse this to answer the user." - - # Insert into chat history as a system message + injection = ( + f"Relevant information:\n{context_str}\n\n" + "Answer using only this. Do not mention these notes or where they came from." + ) turn_ctx.add_message(role="system", content=injection) - logger.info(f"Injected context: {context_str[:100]}...") # Log first 100 chars + logger.info(f"Injected context ({took_ms:.1f}ms): {context_str[:100]}...") else: + # No match: keep the agent from inventing an answer. + turn_ctx.add_message( + role="system", + content="No relevant information was found. Say you don't have that detail " + "and offer to connect them with a person. Do not make up specifics.", + ) logger.info("No relevant context found in Moss index") - + except Exception as e: logger.error(f"Moss search failed: {e}", exc_info=True) @@ -73,32 +138,68 @@ async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatM async def entrypoint(ctx: JobContext): + if not MOSS_PROJECT_ID or not MOSS_PROJECT_KEY: + raise SystemExit( + "Missing MOSS_PROJECT_ID / MOSS_PROJECT_KEY. Copy .env.example to .env and fill them in." + ) await ctx.connect() # Initialize Moss moss_client = MossClient(project_id=MOSS_PROJECT_ID, project_key=MOSS_PROJECT_KEY) - - # Pre-load index + + # Pre-load the index locally. This is required: region metadata filtering is + # only applied to locally loaded indexes (a cloud-fallback query silently + # ignores the filter), so a failed load must be fatal rather than a warning. try: await moss_client.load_index(INDEX_NAME) logger.info(f"Successfully loaded index: {INDEX_NAME}") except Exception as e: - logger.warning(f"Index not found or failed to load: {e}") - logger.warning("Moss queries will fail until the index is created. Run upload.py first.") + raise SystemExit( + f"Failed to load index '{INDEX_NAME}': {e}. Run seed_index.py first " + "(region metadata filtering needs the index loaded locally)." + ) # Create Session session = AgentSession( - stt=deepgram.STT(), - llm=openai.LLM(model="gpt-4o"), - tts=cartesia.TTS(model="sonic-3-2026-01-12"), - vad=silero.VAD.load(), - turn_handling={"interruption": {"mode": "vad"}}, + stt=deepgram.STT(model="nova-2", language="en-US"), + llm=openai.LLM(model="gpt-4o-mini"), + # sonic-turbo = Cartesia's lowest-latency model; "Jacqueline" voice. + # Swap the id for any voice from play.cartesia.ai. + tts=cartesia.TTS(model="sonic-turbo", voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc"), + # activation_threshold above the 0.5 default + a short silence window + # cuts false triggers so the agent doesn't talk over the caller. + vad=silero.VAD.load(min_silence_duration=0.5, activation_threshold=0.6), + # A real turn-detection model + endpointing delays make turn-taking + # feel crisp instead of guessing on raw VAD. + turn_handling={ + "turn_detection": MultilingualModel(), + "endpointing": {"min_delay": 0.5, "max_delay": 1.5}, + }, ) + agent = MossSemanticRetrievalAgent(moss_client, ctx.room) + + # The UI region picker publishes { "region": "US" | "EU" } on this topic. + @ctx.room.on("data_received") + def _on_data(pkt: rtc.DataPacket): + if pkt.topic == "moss.region": + try: + r = json.loads(bytes(pkt.data).decode("utf-8")).get("region") + if r in ALLOWED_REGIONS: + agent.region = r + logger.info(f"Region filter set to {r}") + else: + logger.warning(f"Ignoring unknown region {r!r} (allowed: {sorted(ALLOWED_REGIONS)})") + except Exception as e: + logger.warning(f"Bad region packet: {e}") + # Start the session with our custom MossSemanticRetrievalAgent - await session.start( - agent=MossSemanticRetrievalAgent(moss_client), - room=ctx.room, + await session.start(agent=agent, room=ctx.room) + + # Speak first, instantly — a fixed opener via say() skips the LLM round-trip. + await session.say( + "Thanks for calling Northwind support. How can I help you today?", + allow_interruptions=True, ) if __name__ == "__main__": diff --git a/moss-live-labs/examples/voice-agent/data/faqs.json b/moss-live-labs/examples/voice-agent/data/faqs.json new file mode 100644 index 00000000..ebe31f16 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/data/faqs.json @@ -0,0 +1,122 @@ +[ + { + "id": "returns-policy", + "category": "returns", + "region": "all", + "text": "Northwind accepts returns for a refund as long as items are unworn, unwashed, and in their original packaging with tags attached. Start a return from the Orders page in your account and print the prepaid label." + }, + { + "id": "return-window-us", + "category": "returns", + "region": "US", + "text": "In the United States, the return window is 30 days from the delivery date. Holiday orders placed in November and December can be returned until January 31. Final-sale and clearance items cannot be returned." + }, + { + "id": "return-window-eu", + "category": "returns", + "region": "EU", + "text": "In the EU, you have a 14-day right of withdrawal starting the day you receive the item, in line with EU consumer law. Return shipping on withdrawals is free, and final-sale items are excluded." + }, + { + "id": "exchange-size", + "category": "returns", + "region": "all", + "text": "If an item doesn't fit, you can exchange it for a different size or color at no extra cost. Choose 'Exchange' instead of 'Refund' when you start the return, and we ship the replacement as soon as the original is scanned by the carrier." + }, + { + "id": "refund-timing", + "category": "returns", + "region": "all", + "text": "Refunds are issued to your original payment method within 5 to 7 business days after we receive and inspect the returned item. You'll get an email confirmation when the refund is processed." + }, + { + "id": "shipping-times", + "category": "shipping", + "region": "all", + "text": "Standard shipping takes 3 to 5 business days. Express shipping arrives in 2 business days, and overnight orders placed before 12pm arrive the next business day. Orders are processed within 24 hours on business days." + }, + { + "id": "shipping-cost-us", + "category": "shipping", + "region": "US", + "text": "In the United States, standard shipping is free on orders over 50 dollars, otherwise a flat 5.95 dollars. Express shipping is 12.95 dollars and overnight is 24.95 dollars." + }, + { + "id": "shipping-cost-eu", + "category": "shipping", + "region": "EU", + "text": "In the EU, standard shipping is free on orders over 50 euros, otherwise a flat 4.95 euros. Express shipping is 11.95 euros. Prices include VAT." + }, + { + "id": "international-shipping", + "category": "shipping", + "region": "all", + "text": "Northwind ships to over 40 countries. International delivery takes 7 to 14 business days. Duties and taxes are calculated at checkout so there are no surprise fees on delivery." + }, + { + "id": "order-tracking", + "category": "orders", + "region": "all", + "text": "Once your order ships you'll receive a tracking number by email. You can also see live tracking on the Orders page in your account. Tracking updates can take up to 24 hours to appear after the label is created." + }, + { + "id": "change-order", + "category": "orders", + "region": "all", + "text": "You can change or cancel an order within 60 minutes of placing it from the Orders page. After that the order enters fulfillment and can't be modified, but you can return it once it arrives." + }, + { + "id": "change-address", + "category": "orders", + "region": "all", + "text": "To update the shipping address, edit the order within 60 minutes of checkout. If it's already being prepared, contact support and we'll try to reroute it with the carrier, though we can't guarantee a change once it has shipped." + }, + { + "id": "duplicate-charge", + "category": "billing", + "region": "all", + "text": "If you see two charges for one order, one of them is almost always a temporary authorization hold that drops off in 3 to 5 business days. If both charges still post as completed, contact support with your order number and we'll refund the duplicate immediately." + }, + { + "id": "payment-methods", + "category": "billing", + "region": "all", + "text": "We accept Visa, Mastercard, American Express, Discover, Apple Pay, Google Pay, and Northwind gift cards. Buy-now-pay-later is available through Affirm at checkout on larger orders." + }, + { + "id": "promo-code", + "category": "billing", + "region": "all", + "text": "Enter promo codes in the 'Discount code' box at checkout. Only one code can be used per order, and codes can't be combined with clearance pricing. If a code won't apply, check the expiration date and any minimum-spend requirement." + }, + { + "id": "account-password", + "category": "account", + "region": "all", + "text": "To reset your password, click 'Forgot password' on the sign-in page and we'll email you a reset link that's valid for one hour. If the email doesn't arrive, check spam or make sure you're using the address on file." + }, + { + "id": "warranty", + "category": "product", + "region": "all", + "text": "Northwind products carry a one-year limited warranty against manufacturing defects. If something fails under normal use within a year, we'll repair or replace it for free. Wear and tear and accidental damage aren't covered." + }, + { + "id": "damaged-item", + "category": "product", + "region": "all", + "text": "If your item arrives damaged or defective, contact us within 14 days with a photo and your order number. We'll send a replacement right away and cover return shipping on the damaged item, no need to wait for a refund." + }, + { + "id": "gift-cards", + "category": "billing", + "region": "all", + "text": "Digital gift cards are delivered by email within minutes and never expire. They can be used across multiple orders, and any remaining balance stays on the card. Gift cards can't be redeemed for cash except where required by law." + }, + { + "id": "support-hours", + "category": "support", + "region": "all", + "text": "Northwind support is available by chat and email 7 days a week, from 8am to 8pm. Phone support runs Monday through Friday, 9am to 6pm. Most email replies go out within a few hours." + } +] diff --git a/moss-live-labs/examples/voice-agent/seed_index.py b/moss-live-labs/examples/voice-agent/seed_index.py new file mode 100644 index 00000000..74f40120 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/seed_index.py @@ -0,0 +1,62 @@ +"""Seed the Moss index the voice agent queries. + +Loads data/faqs.json into a Moss index so the agent has a knowledge base to +retrieve from. Run this once before starting the agent: + + python seed_index.py +""" + +import asyncio +import json +import logging +import os +from pathlib import Path + +from dotenv import load_dotenv +from moss import MossClient, DocumentInfo + +load_dotenv() + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("moss-seed") + +MOSS_PROJECT_ID = os.getenv("MOSS_PROJECT_ID") +MOSS_PROJECT_KEY = os.getenv("MOSS_PROJECT_KEY") +INDEX_NAME = os.getenv("MOSS_INDEX_NAME", "demo-customer_faqs") + +FAQS_PATH = Path(__file__).parent / "data" / "faqs.json" + + +async def main() -> None: + if not MOSS_PROJECT_ID or not MOSS_PROJECT_KEY: + raise SystemExit( + "Missing MOSS_PROJECT_ID / MOSS_PROJECT_KEY. Copy .env.example to .env and fill them in." + ) + + faqs = json.loads(FAQS_PATH.read_text()) + docs = [ + DocumentInfo( + id=f["id"], + text=f["text"], + metadata={"category": f.get("category", ""), "region": f.get("region", "all")}, + ) + for f in faqs + ] + + client = MossClient(project_id=MOSS_PROJECT_ID, project_key=MOSS_PROJECT_KEY) + + logger.info(f"Creating index '{INDEX_NAME}' with {len(docs)} documents...") + try: + await client.create_index(INDEX_NAME, docs) + logger.info("Index created. The agent can now answer from this knowledge base.") + except Exception as e: + logger.error(f"Failed to create index: {e}") + logger.error( + "If the index already exists, delete it in the Moss portal (or use a new " + "MOSS_INDEX_NAME) and run this again." + ) + raise + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/moss-live-labs/examples/voice-agent/web/.env.local.example b/moss-live-labs/examples/voice-agent/web/.env.local.example new file mode 100644 index 00000000..d296dc41 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/.env.local.example @@ -0,0 +1,3 @@ +LIVEKIT_URL=ws://localhost:7880 +LIVEKIT_API_KEY=devkey +LIVEKIT_API_SECRET=secret diff --git a/moss-live-labs/examples/voice-agent/web/.gitignore b/moss-live-labs/examples/voice-agent/web/.gitignore new file mode 100644 index 00000000..2e4bfc52 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/.gitignore @@ -0,0 +1,4 @@ +node_modules +.next +.env.local +next-env.d.ts diff --git a/moss-live-labs/examples/voice-agent/web/app/api/token/route.ts b/moss-live-labs/examples/voice-agent/web/app/api/token/route.ts new file mode 100644 index 00000000..80819da3 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/app/api/token/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from "next/server"; +import { AccessToken, type VideoGrant } from "livekit-server-sdk"; + +// Copy web/.env.local.example to web/.env.local to get the `livekit-server --dev` defaults. +const LIVEKIT_URL = process.env.LIVEKIT_URL; +const API_KEY = process.env.LIVEKIT_API_KEY; +const API_SECRET = process.env.LIVEKIT_API_SECRET; + +export const revalidate = 0; + +// Local-dev demo: this endpoint is intentionally unauthenticated. Before deploying +// beyond localhost, gate it behind your own auth so it can't mint room tokens for +// anonymous callers. +export async function GET() { + try { + if (!LIVEKIT_URL) throw new Error("LIVEKIT_URL is not defined"); + if (!API_KEY) throw new Error("LIVEKIT_API_KEY is not defined"); + if (!API_SECRET) throw new Error("LIVEKIT_API_SECRET is not defined"); + + // collision-resistant so concurrent visitors never share a room (and its audio / moss.retrieval data) + const roomName = `support-demo-${crypto.randomUUID()}`; + const identity = `user-${crypto.randomUUID()}`; + + const at = new AccessToken(API_KEY, API_SECRET, { identity, name: "You", ttl: "15m" }); + const grant: VideoGrant = { + room: roomName, + roomJoin: true, + canPublish: true, // publish mic + canPublishData: true, + canSubscribe: true, + }; + at.addGrant(grant); + + return NextResponse.json( + { serverUrl: LIVEKIT_URL, roomName, participantToken: await at.toJwt() }, + { headers: { "Cache-Control": "no-store" } }, + ); + } catch (error) { + const msg = error instanceof Error ? error.message : "Unknown error"; + return new NextResponse(msg, { status: 500 }); + } +} diff --git a/moss-live-labs/examples/voice-agent/web/app/globals.css b/moss-live-labs/examples/voice-agent/web/app/globals.css new file mode 100644 index 00000000..c753f151 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/app/globals.css @@ -0,0 +1,376 @@ +:root { + --bg: #0f1410; + --panel: #131a14; + --panel-soft: #0c110d; + --text: #f1f1f1; + --muted: #8b978c; + --faint: #5a6459; + --sage: #7ea97e; + --mint: #abe6c7; + --lime: #e6f1a8; + --border: rgba(171, 230, 199, 0.14); + --dot: rgba(126, 169, 126, 0.12); + --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + --font-mono: var(--font-geist-mono), ui-monospace, monospace; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + height: 100%; +} + +body { + background-color: var(--bg); + background-image: radial-gradient(var(--dot) 1.5px, transparent 1.5px); + background-size: 40px 40px; + color: var(--text); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; +} + +.app { + min-height: 100vh; + display: flex; + flex-direction: column; + max-width: 1280px; + margin: 0 auto; + padding: 24px; + gap: 20px; +} + +/* ---- header ---- */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 6px; +} +.brand { + display: flex; + align-items: center; + gap: 14px; +} +.brand img { + height: 30px; + width: auto; +} +.brand .divider { + width: 1px; + height: 22px; + background: var(--border); +} +.brand .title { + font-size: 15px; + color: var(--muted); + letter-spacing: 0.2px; +} +.status { + display: flex; + align-items: center; + gap: 9px; + font-family: var(--font-mono); + font-size: 13px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 1.5px; +} +.status .dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--faint); +} +.status.live .dot { + background: var(--mint); + box-shadow: 0 0 10px var(--mint); +} + +/* ---- main split ---- */ +.main { + flex: 1; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + min-height: 0; +} +@media (max-width: 900px) { + .main { + grid-template-columns: 1fr; + } +} + +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 20px; + padding: 22px; + display: flex; + flex-direction: column; + min-height: 0; +} + +/* ---- agent side ---- */ +.agent { + align-items: center; + gap: 18px; +} +.orb { + width: 100%; + height: 190px; + display: flex; + align-items: center; + justify-content: center; +} +.agent-state { + font-family: var(--font-mono); + font-size: 13px; + letter-spacing: 2px; + text-transform: uppercase; + color: var(--mint); +} +.agent-state.idle { + color: var(--faint); +} + +/* transcript */ +.transcript { + width: 100%; + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 12px; + padding: 6px 2px; +} +.turn { + display: flex; + flex-direction: column; + gap: 3px; +} +.turn .who { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--faint); +} +.turn.user .who { + color: var(--sage); +} +.turn.agent .who { + color: var(--mint); +} +.turn .text { + font-size: 17px; + line-height: 1.5; + color: var(--text); +} +.transcript-empty { + margin: auto; + color: var(--faint); + font-size: 15px; + text-align: center; +} + +/* ---- retrieval side ---- */ +.retrieval-head { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-mono); + font-size: 13px; + letter-spacing: 3px; + text-transform: uppercase; + color: var(--mint); + margin-bottom: 4px; +} +.retrieval-head .diamond { + width: 12px; + height: 12px; + background: var(--mint); + border-radius: 2px; + rotate: 45deg; +} +.retrieval-query { + font-size: 18px; + color: var(--text); + margin: 14px 0 6px; + min-height: 27px; +} +.retrieval-query .label { + font-family: var(--font-mono); + font-size: 13px; + color: var(--faint); + margin-right: 8px; +} +.chunks { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 8px; +} +.chunk { + background: var(--panel-soft); + border: 1px solid var(--border); + border-radius: 12px; + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 8px; +} +.chunk .top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.chunk .score { + font-family: var(--font-mono); + font-size: 13px; + color: var(--mint); +} +.chunk .bar { + flex: 1; + height: 4px; + border-radius: 3px; + background: rgba(171, 230, 199, 0.12); + overflow: hidden; +} +.chunk .bar > span { + display: block; + height: 100%; + background: var(--mint); + border-radius: 3px; +} +.chunk .text { + font-size: 14px; + line-height: 1.45; + color: var(--muted); +} +.retrieval-foot { + font-family: var(--font-mono); + font-size: 13px; + color: var(--muted); + margin-top: 12px; + display: flex; + gap: 8px; + align-items: center; +} +.retrieval-foot .ms { + color: var(--mint); +} +.retrieval-empty { + margin: auto; + color: var(--faint); + font-size: 15px; + text-align: center; + max-width: 260px; + line-height: 1.5; +} + +/* ---- footer ---- */ +.footer { + display: flex; + align-items: center; + justify-content: center; + gap: 20px; + font-family: var(--font-mono); + font-size: 13px; + color: var(--muted); +} +.footer a { + color: var(--muted); + text-decoration: none; +} +.footer a:hover { + color: var(--mint); +} +.footer .sep { + color: var(--faint); +} + +/* ---- connect screen ---- */ +.connect { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 26px; +} +.connect h1 { + font-size: 46px; + font-weight: 700; + letter-spacing: -1.5px; + margin: 0; + max-width: 720px; + line-height: 1.1; +} +.connect h1 .accent { + color: var(--mint); +} +.connect p { + color: var(--muted); + font-size: 18px; + margin: 0; + max-width: 520px; + line-height: 1.5; +} +.btn { + font-family: var(--font-sans); + font-size: 17px; + font-weight: 600; + color: var(--bg); + background: var(--mint); + border: none; + border-radius: 999px; + padding: 15px 34px; + cursor: pointer; + transition: transform 0.12s ease, opacity 0.12s ease; +} +.btn:hover { + transform: translateY(-1px); +} +.btn:disabled { + opacity: 0.6; + cursor: default; +} + +/* ---- LiveKit overrides ---- */ +.lk-audio-bar { + background-color: var(--mint) !important; +} +.orb .lk-audio-visualizer, +.orb [data-lk-va-state] { + width: 100%; + height: 100%; +} +.lk-agent-control-bar, +.lk-control-bar { + border: none !important; + background: transparent !important; + padding: 0 !important; + justify-content: center; + gap: 10px; +} +.lk-button { + background: var(--panel-soft) !important; + color: var(--text) !important; + border: 1px solid var(--border) !important; + border-radius: 999px !important; +} +.lk-button:hover { + border-color: var(--mint) !important; +} +.lk-button.lk-button-menu[data-lk-enabled="true"] { + color: var(--mint) !important; +} diff --git a/moss-live-labs/examples/voice-agent/web/app/layout.tsx b/moss-live-labs/examples/voice-agent/web/app/layout.tsx new file mode 100644 index 00000000..5b22d909 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/app/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import { GeistSans } from "geist/font/sans"; +import { GeistMono } from "geist/font/mono"; +import "@livekit/components-styles"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Moss · Support Voice Agent", + description: "A customer-support voice agent grounded in a Moss knowledge base.", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/moss-live-labs/examples/voice-agent/web/app/page.tsx b/moss-live-labs/examples/voice-agent/web/app/page.tsx new file mode 100644 index 00000000..b0986702 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/app/page.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { LiveKitRoom, RoomAudioRenderer } from "@livekit/components-react"; +import { AgentSide } from "@/components/AgentSide"; +import { RetrievalPanel } from "@/components/RetrievalPanel"; + +type Conn = { serverUrl: string; roomName: string; participantToken: string }; + +export default function Page() { + const [conn, setConn] = useState(null); + const [connecting, setConnecting] = useState(false); + + const connect = useCallback(async () => { + setConnecting(true); + try { + const res = await fetch("/api/token"); + if (!res.ok) throw new Error(await res.text()); + const data = (await res.json()) as Conn; + setConn(data); + } catch (err) { + console.error("failed to get token", err); + alert("Could not reach the token endpoint. Is the app running and LiveKit reachable?"); + } finally { + setConnecting(false); + } + }, []); + + return ( +
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Moss + + Northwind Support · voice agent +
+
+ + {conn ? "live" : "offline"} +
+
+ + {conn ? ( + setConn(null)} + > + + + + + ) : ( +
+
+

+ Talk to a support agent that actually knows the answers. +

+

+ Grounded in a Moss knowledge base. Ask a question out loud and watch Moss retrieve the + answer live, on-device, in milliseconds. +

+ +
+
+ )} + + +
+ ); +} diff --git a/moss-live-labs/examples/voice-agent/web/components/AgentSide.tsx b/moss-live-labs/examples/voice-agent/web/components/AgentSide.tsx new file mode 100644 index 00000000..fbd35b5f --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/components/AgentSide.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { + BarVisualizer, + VoiceAssistantControlBar, + useVoiceAssistant, +} from "@livekit/components-react"; +import { Transcript } from "./Transcript"; + +const STATE_LABEL: Record = { + listening: "listening", + thinking: "thinking", + speaking: "speaking", + connecting: "connecting", + initializing: "warming up", +}; + +export function AgentSide() { + const { state, audioTrack } = useVoiceAssistant(); + const label = STATE_LABEL[state] ?? "idle"; + const idle = !state || state === "disconnected"; + + return ( +
+
+ +
+
{label}
+ + +
+ ); +} diff --git a/moss-live-labs/examples/voice-agent/web/components/RetrievalPanel.tsx b/moss-live-labs/examples/voice-agent/web/components/RetrievalPanel.tsx new file mode 100644 index 00000000..f6c055c0 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/components/RetrievalPanel.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { + useDataChannel, + useRoomContext, + useConnectionState, +} from "@livekit/components-react"; +import { ConnectionState } from "livekit-client"; +import type { RetrievalPayload } from "@/lib/types"; + +const REGIONS = ["US", "EU"] as const; +type Region = (typeof REGIONS)[number]; + +// Shows what Moss retrieved for the latest turn, plus a region picker that +// live-updates the metadata filter the agent applies (no restart needed). +export function RetrievalPanel() { + const room = useRoomContext(); + const connState = useConnectionState(); + const [data, setData] = useState(null); + const [region, setRegion] = useState("US"); + + useDataChannel( + "moss.retrieval", + useCallback((msg: { payload: Uint8Array }) => { + try { + setData(JSON.parse(new TextDecoder().decode(msg.payload)) as RetrievalPayload); + } catch (err) { + console.error("failed to parse moss.retrieval payload", err); + } + }, []), + ); + + const publishRegion = useCallback( + (r: Region) => { + try { + room.localParticipant?.publishData( + new TextEncoder().encode(JSON.stringify({ region: r })), + { reliable: true, topic: "moss.region" }, + ); + } catch (err) { + console.error("failed to publish region", err); + } + }, + [room], + ); + + // Sync the agent to the picker whenever we connect or the region changes. + useEffect(() => { + if (connState === ConnectionState.Connected) publishRegion(region); + }, [connState, region, publishRegion]); + + return ( +
+
+ + + Moss · knowledge base + + + {REGIONS.map((r) => ( + + ))} + +
+ +
+ region: {region} + global +
+ + {data ? ( + <> +
+ query + {data.query} +
+
+ {data.docs.map((doc, i) => { + const pct = Math.max(0, Math.min(1, doc.score)) * 100; + return ( +
+
+
+ +
+ {doc.score.toFixed(2)} +
+
{doc.text}
+
+ ); + })} +
+
+ {data.took_ms.toFixed(1)}ms + · + + {data.docs.length} result{data.docs.length === 1 ? "" : "s"} · on-device + +
+ + ) : ( +
+ Pick a region, then ask the agent a question. The chunks Moss retrieves for that region appear here. +
+ )} +
+ ); +} diff --git a/moss-live-labs/examples/voice-agent/web/components/Transcript.tsx b/moss-live-labs/examples/voice-agent/web/components/Transcript.tsx new file mode 100644 index 00000000..19ab1639 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/components/Transcript.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRoomContext } from "@livekit/components-react"; +import { RoomEvent, type TranscriptionSegment, type Participant } from "livekit-client"; + +type Turn = { id: string; text: string; isUser: boolean }; + +// Renders live STT (user) + TTS (agent) transcriptions. Keyed by segment id so +// interim results update in place; object insertion order preserves turn order. +export function Transcript() { + const room = useRoomContext(); + const [turns, setTurns] = useState>({}); + + useEffect(() => { + const onTranscription = (segments: TranscriptionSegment[], participant?: Participant) => { + setTurns((prev) => { + const next = { ...prev }; + for (const seg of segments) { + next[seg.id] = { id: seg.id, text: seg.text, isUser: Boolean(participant?.isLocal) }; + } + // cap history so a long call doesn't grow memory without bound + const ids = Object.keys(next); + const MAX = 100; + if (ids.length > MAX) { + for (const id of ids.slice(0, ids.length - MAX)) delete next[id]; + } + return next; + }); + }; + room.on(RoomEvent.TranscriptionReceived, onTranscription); + return () => { + room.off(RoomEvent.TranscriptionReceived, onTranscription); + }; + }, [room]); + + const ordered = Object.values(turns).filter((t) => t.text.trim().length > 0); + + if (ordered.length === 0) { + return
Say hello to start the conversation.
; + } + + return ( +
+ {ordered.map((turn) => ( +
+ {turn.isUser ? "You" : "Agent"} + {turn.text} +
+ ))} +
+ ); +} diff --git a/moss-live-labs/examples/voice-agent/web/lib/types.ts b/moss-live-labs/examples/voice-agent/web/lib/types.ts new file mode 100644 index 00000000..e5555ba0 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/lib/types.ts @@ -0,0 +1,12 @@ +export type RetrievalDoc = { + id?: string; + text: string; + score: number; +}; + +export type RetrievalPayload = { + query: string; + docs: RetrievalDoc[]; + took_ms: number; + region?: string; +}; diff --git a/moss-live-labs/examples/voice-agent/web/next.config.mjs b/moss-live-labs/examples/voice-agent/web/next.config.mjs new file mode 100644 index 00000000..9e248806 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/next.config.mjs @@ -0,0 +1,14 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Node 18 doesn't provide import.meta.dirname; derive it from import.meta.url. +const rootDir = dirname(fileURLToPath(import.meta.url)); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + // This app lives in a monorepo with other lockfiles; pin the tracing root here. + outputFileTracingRoot: rootDir, +}; + +export default nextConfig; diff --git a/moss-live-labs/examples/voice-agent/web/package-lock.json b/moss-live-labs/examples/voice-agent/web/package-lock.json new file mode 100644 index 00000000..b9446e7c --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/package-lock.json @@ -0,0 +1,1329 @@ +{ + "name": "voice-agent-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "voice-agent-web", + "version": "0.1.0", + "dependencies": { + "@livekit/components-react": "^2.9.11", + "@livekit/components-styles": "^1.1.6", + "geist": "^1.3.1", + "livekit-client": "^2.15.4", + "livekit-server-sdk": "^2.13.1", + "next": "^15.5.4", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@livekit/components-core": { + "version": "0.12.13", + "resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz", + "integrity": "sha512-DQmi84afHoHjZ62wm8y+XPNIDHTwFHAltjd3lmyXj8UZHOY7wcza4vFt1xnghJOD5wLRY58L1dkAgAw59MgWvw==", + "license": "Apache-2.0", + "dependencies": { + "@floating-ui/dom": "1.7.4", + "loglevel": "1.9.1", + "rxjs": "7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "livekit-client": "^2.17.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@livekit/components-react": { + "version": "2.9.21", + "resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.21.tgz", + "integrity": "sha512-6hU9VucJJL+gAhilNGe4MBCDCZVk64qyjP9Ck86krvOIdVU76WeWksddg1MYUP10AlUwwrfD7davz41pJTcMJw==", + "license": "Apache-2.0", + "dependencies": { + "@livekit/components-core": "0.12.13", + "clsx": "2.1.1", + "events": "^3.3.0", + "jose": "^6.0.12", + "usehooks-ts": "3.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@livekit/krisp-noise-filter": "^0.2.12 || ^0.3.0", + "livekit-client": "^2.18.2", + "react": ">=18", + "react-dom": ">=18", + "tslib": "^2.6.2" + }, + "peerDependenciesMeta": { + "@livekit/krisp-noise-filter": { + "optional": true + } + } + }, + "node_modules/@livekit/components-styles": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@livekit/components-styles/-/components-styles-1.2.0.tgz", + "integrity": "sha512-74/rt0lDh6aHmOPmWAeDE9C4OrNW9RIdmhX/YRbovQBVNGNVWojRjl3FgQZ5LPFXO6l1maKB4JhXcBFENVxVvw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@livekit/mutex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.1.1.tgz", + "integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==", + "license": "Apache-2.0" + }, + "node_modules/@livekit/protocol": { + "version": "1.46.6", + "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.46.6.tgz", + "integrity": "sha512-upzlHP1vi/kZ/QqALZTFskQ0ifqc2f15RKucHYOsIHJsaXvEYanG75mAb7o+Yomfs4XhQ4BaRsdY+TFHXpaqrg==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.20.tgz", + "integrity": "sha512-dXh51Wvddf8daEyBXryZZEe1FdVxEWx9lgaTseLZUtC1XP/W8Wri+Z+VPOElHlByk23CyqHdc2oVByX7wsTWsw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.20.tgz", + "integrity": "sha512-in0yXG7/pRBVjWeEl7f7ZZETpletSMFKXVS4GJgHENTPVrJFNJKPrYewa9rpZcvdjwFece5fZP0CK34G4PxowA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.20.tgz", + "integrity": "sha512-0hsFshdPnTzGJdDTHeHJ+XPUShOpnyp9pUFDwDhqctsA0Cd8NcIVGRPtptYhgYY9DjkKgCDRkXxmgRc+CgT5Wg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.20.tgz", + "integrity": "sha512-DMvkoBtAABOzE6pMZRW/xNm7sKqql3wzzzZJ1R/d/rp4BCxv6LykouD3tHjGY8WdQqGpZs11t+R9AtjPxvvljw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.20.tgz", + "integrity": "sha512-RQmDfeYBtXV2FSId7dfA1hE6M/T6+g7wdbYnFQ47tw/gUBwV+CccLVejNmCGa9yLDitk83foeg8hl/3DjfYQ5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.20.tgz", + "integrity": "sha512-DkWLEdKajJwdGt27M3i1VEO2kelTvZrK6Pcb7JvW2BY+nofWm7FBsBNDj7g7Pr1NuQ5PLJvqEqYa20GTsBDnKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.20.tgz", + "integrity": "sha512-rAO5b7pKHvX+ExdmJskusDXTNbiNZfptifIPZItbUx+AOXxxTydVBsPt7Oz84DRd5mY8e0DcE8kvLj3AIfjE6w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.20.tgz", + "integrity": "sha512-Hp3zFsN8N8Kj9+vY6L4vnZ9EtA9eXyATu0q4EfGbZTiocgPUNSfz8NWhym6xvaOmHpJ8EuoypuU1WejCPsTFtg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.20.tgz", + "integrity": "sha512-T/L7CXpR1M0wij/xbF3rT1+7KvSkfOLr7C+ToHHWZTG2eKmb52C5WvsyGCBNtkVvDEUESWkRUbbqSH4rSbOCYQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/dom-mediacapture-record": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", + "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-9.1.3.tgz", + "integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==", + "license": "MIT", + "dependencies": { + "camelcase": "^8.0.0", + "map-obj": "5.0.0", + "quick-lru": "^6.1.1", + "type-fest": "^4.3.2" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "Apache-2.0", + "optional": true, + "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/geist": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/geist/-/geist-1.7.2.tgz", + "integrity": "sha512-Gu5lDFa3pLRyoBlBPf0QIFHVdWAnpco7fS1bJm41jyLPFoguBgiubseUN2oLXMgqZ7uxAxDoXcHMhCY/fOTTgg==", + "license": "SIL OPEN FONT LICENSE", + "peerDependencies": { + "next": ">=13.2.0" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/livekit-client": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.20.0.tgz", + "integrity": "sha512-RIJcpvBmOmwz3jTj3rmdY6Dzr55HrhcaJjMgY+HSmoEM+yIRyA40m7r8UKv0hnZWM3z/AYhP1q8C8ciz5UWFKQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@livekit/mutex": "1.1.1", + "@livekit/protocol": "1.46.6", + "events": "^3.3.0", + "jose": "^6.1.0", + "loglevel": "^1.9.2", + "sdp-transform": "^2.15.0", + "tslib": "2.8.1", + "typed-emitter": "^2.1.0", + "webrtc-adapter": "9.0.5" + }, + "peerDependencies": { + "@types/dom-mediacapture-record": "^1" + } + }, + "node_modules/livekit-client/node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/livekit-server-sdk": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-2.16.0.tgz", + "integrity": "sha512-Myr90TWbqUujvPpo0N8JS+gtY6jVKgbIRx688cVSL3mp18IVAwhDWyRzNloQBcimHg24zz1JjERTiwOw0wuFIg==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.1", + "@livekit/protocol": "^1.48.0", + "camelcase-keys": "^9.0.0", + "jose": "^5.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/livekit-server-sdk/node_modules/@livekit/protocol": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.49.0.tgz", + "integrity": "sha512-TCmihwjHBWAUdNrqvmrN/K59r7vDnPIy3OEaE3p0g4FvdcQTiLynmFC3igR25Q2fnifahhAJjnnfSCtWUK7kYg==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/livekit-server-sdk/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/loglevel": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/map-obj": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz", + "integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/next": { + "version": "15.5.20", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.20.tgz", + "integrity": "sha512-cvyS3/geydan1xLtE3FA8VCgdoQ/Gg/dlOldFkFCbB5VcVYJV7090hQLBnvTW2PwT76Z/dHdzDZCsVhZpoOlUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@next/env": "15.5.20", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.20", + "@next/swc-darwin-x64": "15.5.20", + "@next/swc-linux-arm64-gnu": "15.5.20", + "@next/swc-linux-arm64-musl": "15.5.20", + "@next/swc-linux-x64-gnu": "15.5.20", + "@next/swc-linux-x64-musl": "15.5.20", + "@next/swc-win32-arm64-msvc": "15.5.20", + "@next/swc-win32-x64-msvc": "15.5.20", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "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", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/sdp": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.2.tgz", + "integrity": "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==", + "license": "MIT" + }, + "node_modules/sdp-transform": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", + "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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", + "peer": true + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/usehooks-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", + "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/webrtc-adapter": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.5.tgz", + "integrity": "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + } + } +} diff --git a/moss-live-labs/examples/voice-agent/web/package.json b/moss-live-labs/examples/voice-agent/web/package.json new file mode 100644 index 00000000..4a4bdf82 --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "voice-agent-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@livekit/components-react": "^2.9.11", + "@livekit/components-styles": "^1.1.6", + "geist": "^1.3.1", + "livekit-client": "^2.15.4", + "livekit-server-sdk": "^2.13.1", + "next": "^15.5.4", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } +} diff --git a/moss-live-labs/examples/voice-agent/web/public/moss-wordmark.svg b/moss-live-labs/examples/voice-agent/web/public/moss-wordmark.svg new file mode 100644 index 00000000..4430181a --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/public/moss-wordmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/moss-live-labs/examples/voice-agent/web/tsconfig.json b/moss-live-labs/examples/voice-agent/web/tsconfig.json new file mode 100644 index 00000000..efcf0b9d --- /dev/null +++ b/moss-live-labs/examples/voice-agent/web/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "baseUrl": ".", + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}