diff --git a/AGENTS.md b/AGENTS.md index 18b50bb..6357442 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,94 +1,101 @@ # Agent Development Guide -This document guides AI agents working on the Agora Conversational AI demo project. +This guide is for coding agents making changes in `agent-quickstart-python`. -## Project Overview +## Start Here -A real-time voice conversation application with AI agents, built with: -- **Frontend**: Next.js 16 + React 19 + TypeScript + Agora Web SDK -- **Backend**: Python FastAPI + Agora Conversational AI Agent SDK +- Read [README.md](./README.md) for setup, supported run modes, and verification. +- Use [ARCHITECTURE.md](./ARCHITECTURE.md) for system-level request flow. +- Use module guides only when working inside that module: + - [web-client/AGENTS.md](./web-client/AGENTS.md) + - [server-python/AGENTS.md](./server-python/AGENTS.md) -## Project Structure +## Current System Shape -``` -. -├── web-client/ # Frontend application (Next.js + React) -└── server-python/ # Backend service (FastAPI + Agora Agent SDK) -``` +- Frontend: Next.js 16, React 19, TypeScript, `agora-rtc-react`, `agora-rtm`, `agora-agent-client-toolkit`, `agora-agent-uikit` +- Local backend: Python FastAPI in `server-python` +- Deployed web backend: Next route handlers in `web-client/app/api` +- Auth: Token007 generated from `AGORA_APP_ID` and `AGORA_APP_CERTIFICATE` +- Default agent config: managed Deepgram STT, OpenAI LLM, and MiniMax TTS + +## Supported Modes + +### Local Python-Backed Development + +- Run from the repo root with `bun run dev` +- Root scripts start: + - FastAPI on `http://localhost:8000` + - Next.js on `http://localhost:3000` +- In this mode, the web app still calls `/api/*`, but the Next route handlers proxy to the Python service through `AGENT_BACKEND_URL=http://localhost:8000` + +### Single-Target Web Deployment + +- Deploy `web-client` as a Next.js app +- `/api/get_config`, `/api/v2/startAgent`, and `/api/v2/stopAgent` run inside the Next app +- Do not assume a separate Python service exists in this mode + +## Routing Ownership + +- UI and RTC/RTM client lifecycle live in `web-client` +- `/api/*` entrypoints for the web app live in `web-client/app/api` +- Python agent lifecycle logic lives in `server-python/src` +- For deployability changes, update both the README and architecture docs if the owner of `/api/*` changes + +## Key Files + +- `README.md`: setup, local vs deploy modes, troubleshooting, verification +- `ARCHITECTURE.md`: top-level environment model +- `web-client/src/components/app.tsx`: conversation UI shell +- `web-client/src/hooks/useAgoraConnection.ts`: RTC, RTM, transcript, and token renewal lifecycle +- `web-client/src/lib/server/agora.ts`: shared server-side token and agent helpers for Next route handlers +- `server-python/src/server.py`: FastAPI entrypoints +- `server-python/src/agent.py`: async Agora agent lifecycle wrapper + +## Working Rules -## Quick Start +- Prefer the smallest change that keeps local mode and deployed mode aligned. +- Do not reintroduce `web-client/proxy.ts`; the current proxy fallback is route-local through `AGENT_BACKEND_URL`. +- Do not assume Zustand or a separate client-side store exists. +- Do not require third-party vendor API keys unless the code actually introduces a non-managed path. +- Keep token expiry and renewal behavior aligned across the Python backend and Next route handlers. + +## Standard Commands + +From the repo root: ```bash -# Install dependencies bun install - -# Start both frontend and backend +bun run doctor +bun run doctor:local bun run dev +bun run verify +bun run verify:local +``` + +Useful narrower checks: + +```bash +bun run verify:web +bun run verify:local:fastapi +bun run verify:web:proxy +bun run verify:backend +``` -# Frontend only (port 3000) -bun run frontend +Inside `web-client/`, use: -# Backend only (port 8000) -bun run backend +```bash +bun run doctor +bun run verify ``` -## Module-Specific Guides - -### Frontend (web-client/) -- [web-client/AGENTS.md](./web-client/AGENTS.md) — AI assistant guide for frontend development -- [web-client/ARCHITECTURE.md](./web-client/ARCHITECTURE.md) — Detailed frontend architecture - -### Backend (server-python/) -- [server-python/AGENTS.md](./server-python/AGENTS.md) — AI assistant guide for backend development -- [server-python/ARCHITECTURE.md](./server-python/ARCHITECTURE.md) — Backend architecture details -- [server-python/README.md](./server-python/README.md) — Backend API documentation - -### System Architecture -- [ARCHITECTURE.md](./ARCHITECTURE.md) — Overall system architecture and data flow - -## Key Technologies - -| Layer | Technologies | -|-------|-------------| -| Frontend | Next.js 16, React 19, TypeScript, Agora Web SDK (RTC + RTM), agora-agent-client-toolkit, Zustand, Tailwind CSS | -| Backend | Python 3.8+, FastAPI, agora-agent-server-sdk, uvicorn | -| Auth | Token007 (AccessToken2) — auto-generated from APP_ID + APP_CERTIFICATE | -| Real-time | Agora RTC (audio) + RTM (messaging/transcription) | -| AI Providers | Deepgram (ASR), OpenAI (LLM), ElevenLabs (TTS) | - -## Common Development Tasks - -### Working on Frontend -See [web-client/AGENTS.md](./web-client/AGENTS.md) for: -- UI component development -- State management patterns (Zustand) -- Agora SDK integration (RTC/RTM) -- API client usage - -### Working on Backend -See [server-python/AGENTS.md](./server-python/AGENTS.md) for: -- API endpoint development -- Agent lifecycle management (start/stop via AgentSession) -- Token generation (`generate_convo_ai_token`) -- ASR/LLM/TTS provider configuration - -### Cross-Module Changes -1. Review [ARCHITECTURE.md](./ARCHITECTURE.md) for system overview and data flow -2. Check both module-specific AGENTS.md files -3. Verify API contracts — frontend calls `/api/*`, proxied to backend on port 8000 -4. Test token flow: backend generates Token007, frontend uses it for RTC/RTM - -## Important Notes - -- Never commit `.env.local` or credentials -- Frontend proxies `/api/*` requests to backend via `web-client/proxy.ts` -- Agent lifecycle is managed by backend (AgentSession), not frontend -- All Agora SDK calls go through `useAgoraConnection.ts` hook on the frontend -- Authentication uses Token007 (AccessToken2) — only `APP_ID` and `APP_CERTIFICATE` are needed -- Backend uses `Agora(area=Area.US, ...)` client with auto Token007 auth - -## Reference Documentation - -- [Agora Conversational AI Docs](https://docs.agora.io/en/conversational-ai/overview) -- [Next.js Docs](https://nextjs.org/docs) -- [FastAPI Docs](https://fastapi.tiangolo.com/) +## Done Criteria + +Before finishing a change: + +1. Run the narrowest relevant verification command. +2. If the change affects the deployable web app, ensure `bun run verify:web` passes. +3. If the change affects local Python-backed development, ensure `bun run verify:local` or the narrower `bun run verify:local:fastapi` / `bun run verify:web:proxy` / `bun run verify:backend` commands pass as appropriate. +4. Treat `server-python/.env.local` as CLI-managed by default. If you change required env vars or setup steps, update both the root README and the module README. +5. Update `README.md` or architecture docs when the developer workflow or request flow changes. + +`bun run verify:local:fastapi` exercises the real FastAPI route layer through Next, but with a fake agent implementation so the check stays deterministic and does not depend on a live managed-agent start. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bc1c27b..cb4dd7c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,66 +1,74 @@ # Agora Conversational AI Demo — Architecture -## System Architecture +This quickstart supports two runtime environments. The UI is the same in both modes, but the owner of `/api/*` changes by environment. + +## Local Python-Backed Development ``` -┌─────────────────────────────────────────────────────────────┐ -│ Frontend │ -│ Next.js 16 + React 19 + TypeScript + Agora Web SDK │ -│ (Port 3000) │ -└──────────────────┬──────────────────────────────────────────┘ - │ /api/* proxy (proxy.ts) - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Backend │ -│ Python FastAPI + Agora Agent SDK │ -│ (Port 8000) │ -└──────────────────┬──────────────────────────────────────────┘ - │ REST API (Token007 auth) - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Agora Cloud Services │ -│ • RTC (Real-Time Communication — audio) │ -│ • RTM (Real-Time Messaging — subtitles/transcription) │ -│ • Conversational AI Engine (ASR + LLM + TTS) │ -└─────────────────────────────────────────────────────────────┘ +Browser + ↓ +Next.js app on :3000 + ↓ +/api/* route handlers proxy through AGENT_BACKEND_URL + ↓ +FastAPI service on :8000 + ↓ +Agora Cloud Services ``` -## Data Flow +- `web-client` owns the browser UI and the `/api/*` entrypoints +- `server-python` owns the actual token generation and agent start/stop logic +- this is the mode used by `bun run dev` + +## Single-Target Web Deployment + +``` +Browser + ↓ +Next.js app + ↓ +/api/* route handlers run in-process + ↓ +Agora Cloud Services +``` + +- `web-client` owns both the UI and the deployed `/api/*` implementation +- `server-python` is not required for this deployment path + +## Shared Conversation Flow ### 1. Connection ``` -User clicks "Start" - → Frontend: GET /api/get_config - → Backend: generate_convo_ai_token(app_id, app_certificate, channel, account) - → Frontend: Join RTC channel + Login RTM with token +Frontend: GET /api/get_config + → Generate Token007 config for a user UID, agent UID, and channel + → Frontend joins RTC and logs into RTM ``` ### 2. Agent Start ``` Frontend: POST /api/v2/startAgent { channelName, rtcUid, userUid } - → Backend: Build AgoraAgent (Deepgram ASR + OpenAI LLM + ElevenLabs TTS) - → Backend: session.start() → agent_id - → Agent joins RTC channel → Frontend receives audio + RTM subtitles + → Build agent session + → Scope remote_uids to the requesting user + → Start session and return agent_id ``` ### 3. Conversation ``` -User speaks → RTC audio → Agora Cloud - → Deepgram (ASR): audio → text - → OpenAI (LLM): text → response - → ElevenLabs (TTS): response → audio - → RTC audio + RTM subtitles → Frontend +User audio → RTC + → Managed ASR, LLM, and TTS pipeline + → Agent audio + RTM transcript events + → UIKit transcript and visualizer in the web app ``` ### 4. Agent Stop ``` Frontend: POST /api/v2/stopAgent { agentId } - → Backend: session.stop() - → Agent leaves channel → Frontend cleanup + → Stop session directly or through stateless fallback + → Client cleans up RTC and RTM state ``` ## API Endpoints @@ -68,14 +76,14 @@ Frontend: POST /api/v2/stopAgent { agentId } | Endpoint | Method | Description | |----------|--------|-------------| | `/get_config` | GET | Generate connection config (Token007, channel, UIDs) | -| `/v2/startAgent` | POST | Start AI agent | -| `/v2/stopAgent` | POST | Stop agent by agent_id | +| `/v2/startAgent` | POST | Start the agent session | +| `/v2/stopAgent` | POST | Stop the agent by `agent_id` | -Frontend calls these as `/api/*`, proxied to backend via `web-client/proxy.ts`. +Frontend calls these as `/api/*`. In local Python mode, the Next handlers proxy to `AGENT_BACKEND_URL`; in Vercel they run in-process inside the Next app. ## Authentication -Token007 (AccessToken2) — generated from `APP_ID` + `APP_CERTIFICATE` only. No API_KEY/API_SECRET needed. The SDK handles token generation and API auth internally. +Token007 (AccessToken2) — generated from `AGORA_APP_ID` + `AGORA_APP_CERTIFICATE` only. No API_KEY/API_SECRET needed. The SDK handles token generation and API auth internally. ## Detailed Documentation diff --git a/README.md b/README.md index 1b2f0dc..a30c088 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,101 @@ # Agora Conversational AI Web Demo -Real-time voice conversation with AI agents, featuring live transcription and log monitoring. +Real-time voice conversation with AI agents, featuring the Agora UIKit transcript experience with two supported runtime modes: + +- local Python-backed development +- single-target web deployment ## Prerequisites - [Bun](https://bun.sh/) (package manager & script runner) - Python 3.8+ +- [Agora CLI](https://www.npmjs.com/package/agoraio-cli) - [Agora Account](https://console.agora.io/) with App ID & App Certificate - Agora project with Conversational AI managed provider support enabled ## Quick Start +### Local Python-Backed Development + ```bash # 1. Install dependencies bun install -# 2. Configure backend -cd server-python -cp .env.example .env.local -# Edit .env.local with your credentials (see Configuration below) +# 2. Login and connect the demo to Agora +agora login +agora project create my-first-voice-agent --feature rtc --feature convoai +agora project use my-first-voice-agent +agora project env write server-python/.env.local --with-secrets # 3. Start services -cd .. bun run dev ``` +`server-python/.env.example` remains the reference for the variables this demo uses. The recommended path is to let the Agora CLI write the real values into `server-python/.env.local`. + +`bun install` is run from the repo root and manages the `web-client` package through the root Bun workspace. + Services will be available at: - Frontend: http://localhost:3000 - Backend: http://localhost:8000 - API Docs: http://localhost:8000/docs +In local development, the browser still calls `/api/*` on the Next app. Those route handlers proxy to the FastAPI backend through `AGENT_BACKEND_URL=http://localhost:8000`, which the root scripts set automatically. + +### Single-Target Web Deployment + +Deploy `web-client` as a Next.js app. In this mode, the Next route handlers serve these endpoints directly: + +- `/api/get_config` +- `/api/v2/startAgent` +- `/api/v2/stopAgent` + +Set these env vars in the deployment target: + +```bash +AGORA_APP_ID=your_agora_app_id +AGORA_APP_CERTIFICATE=your_agora_app_certificate +AGENT_GREETING=optional_custom_greeting +``` + +Do not set `AGENT_BACKEND_URL` in deployment unless you intentionally want the web app to proxy to an external Python service. + ## Configuration -Edit `server-python/.env.local`: +Recommended: + +```bash +agora project env write server-python/.env.local --with-secrets +``` + +Reference template: ```bash # Agora Credentials (required) -APP_ID=your_agora_app_id -APP_CERTIFICATE=your_agora_app_certificate +AGORA_APP_ID=your_agora_app_id +AGORA_APP_CERTIFICATE=your_agora_app_certificate PORT=8000 ``` -Authentication uses Token007 (AccessToken2), generated automatically from `APP_ID` and `APP_CERTIFICATE`. Vendor credentials are no longer required in local setup; the backend defaults to the same DeepgramSTT + OpenAI + MiniMaxTTS managed configuration used by the current Next.js quickstart. +Authentication uses Token007 (AccessToken2), generated automatically from `AGORA_APP_ID` and `AGORA_APP_CERTIFICATE`. Vendor credentials are no longer required in local setup; the backend defaults to the same DeepgramSTT + OpenAI + MiniMaxTTS managed configuration used by the current Next.js quickstart. -Frontend gets all configuration from the backend API — no environment variables required on the frontend side. +Frontend deployment env vars live in the deployment target or `web-client/.env.local` when running the web app by itself. The browser does not need its own public Agora credentials in this sample. ## Commands ```bash bun run dev # Start both frontend and backend +bun run doctor # Shared repo checks for any mode +bun run doctor:local # Local Python-backed checks, including required env values bun run backend # Backend only (port 8000) bun run frontend # Frontend only (port 3000) bun run build # Build frontend for production +bun run verify # Verify the single-target web deployment path +bun run verify:local # Verify backend compile + FastAPI app proxy smoke with the real route layer + web build +bun run verify:web # Run web route contract checks + web build +bun run verify:local:fastapi # Smoke-test Next -> FastAPI app for get_config/start/stop +bun run verify:backend # Compile-check the Python backend bun run clean # Clean build artifacts and venvs ``` @@ -71,9 +114,41 @@ bun run clean # Clean build artifacts and venvs | Problem | Check | |---------|-------| | Connection issues | Backend running on port 8000? | -| Auth errors | `APP_ID` and `APP_CERTIFICATE` correct in `.env.local`? | +| Agora credentials not written yet | Run `agora project use my-first-voice-agent` and `agora project env write server-python/.env.local --with-secrets` | +| Auth errors | `AGORA_APP_ID` and `AGORA_APP_CERTIFICATE` correct in `.env.local`? | | Agent fails to start | Confirm Agora managed provider access is enabled for this project, then check logs at http://localhost:8000/docs | -| Frontend can't reach backend | Proxy config in `web-client/proxy.ts` | +| Frontend can't reach backend | If running local Python mode, confirm `AGENT_BACKEND_URL=http://localhost:8000` is set via the root frontend scripts | +| `bun install` did not update the web app | Run it from the repo root; this repo uses a Bun workspace rooted here | +| Deployed web app returns API auth errors | Confirm `AGORA_APP_ID` and `AGORA_APP_CERTIFICATE` are set in the deployment target and `AGENT_BACKEND_URL` is not pointing to localhost | +| Unsure which service owns `/api/*` | Local dev: Next route handlers proxy to FastAPI. Deployment: Next route handlers handle requests directly unless `AGENT_BACKEND_URL` is set | + +## Verification + +Run the mode-appropriate command from the repo root after changes: + +```bash +bun run verify:web +bun run verify:local +``` + +When working inside `web-client` as a standalone deployable app: + +```bash +cd web-client +bun run doctor +bun run verify +``` + +Useful narrower checks: + +```bash +bun run doctor +bun run doctor:local +bun run verify:web +bun run verify:local:fastapi +bun run verify:web:proxy +bun run verify:backend +``` ## Documentation diff --git a/bun.lock b/bun.lock index 2ac18d7..772bcfa 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "agora-conversational-ai-demo", @@ -7,56 +8,588 @@ "concurrently": "^8.2.2", }, }, + "web-client": { + "name": "agora-conversational-ai-demo", + "version": "1.0.0", + "dependencies": { + "agora-agent-client-toolkit": "1.2.0", + "agora-agent-server-sdk": "^1.3.2", + "agora-agent-uikit": "1.1.0", + "agora-rtc-react": "^2.5.1", + "agora-rtc-sdk-ng": "^4.24.3", + "agora-rtm": "^2.2.3", + "agora-token": "^2.0.5", + "next": "^16.1.6", + "react": "^19.2.4", + "react-dom": "^19.2.4", + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0", + "@types/node": "^25.3.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "autoprefixer": "^10.4.18", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.0", + }, + }, }, "packages": { + "@agora-js/media": ["@agora-js/media@4.24.3", "", { "dependencies": { "@agora-js/report": "4.24.3", "@agora-js/shared": "4.24.3", "agora-rte-extension": "^1.2.4", "axios": "^1.13.6", "webrtc-adapter": "8.2.0" } }, "sha512-aC471uaWWGSXnruoaJggi9Ltbp3ldYxMyIi61jOew960zVsHaw78YhTDVv27XX1NvPRqEPLyfyMGEX4av0xOKA=="], + + "@agora-js/protocol": ["@agora-js/protocol@4.24.3", "", { "dependencies": { "@agora-js/media": "4.24.3", "@agora-js/report": "4.24.3", "@agora-js/shared": "4.24.3", "@bufbuild/protobuf": "^2.9.0", "@types/pako": "^2.0.0", "pako": "^2.1.0" } }, "sha512-GWAV+J9wv53u5T/k4KAjsxQ9Y+n73bWc3dSBe7OHjWcSZQikcWRJoOtygQiZKWmpYV/ZW5LQ0R12jHI7GAG0cQ=="], + + "@agora-js/report": ["@agora-js/report@4.24.3", "", { "dependencies": { "@agora-js/shared": "4.24.3", "axios": "^1.13.6" } }, "sha512-A2Q13JbRlSYkEF0W6eMCJmnzmHSBzaWZORNnsS23GElwACSxoa2KTMcDp0MQhlsI9Ht6277ZuVSCdkT48A6Pwg=="], + + "@agora-js/shared": ["@agora-js/shared@4.24.3", "", { "dependencies": { "axios": "^1.13.6", "ua-parser-js": "^0.7.34" } }, "sha512-PyKelGEuV/g4fTjDjANpBqPZMaY0NFdd0W4UcGtJ2rayVU+YAeztLvu4tpDbm14U4BG5dmz/N+VHckxFPgoIjQ=="], + + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.0", "", {}, "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@lottiefiles/dotlottie-react": ["@lottiefiles/dotlottie-react@0.17.15", "", { "dependencies": { "@lottiefiles/dotlottie-web": "0.63.0" }, "peerDependencies": { "react": "^17 || ^18 || ^19" } }, "sha512-4wYAjsJhM28eUvJ/gT3KRM6fcyT7EM9n7PDrP71LaBTacc6bSN43qFTSJc1Li3QxUiraz23p0Q8EJBzXo8DsRw=="], + + "@lottiefiles/dotlottie-web": ["@lottiefiles/dotlottie-web@0.63.0", "", {}, "sha512-oYIkvu6E4n8fZH7ciQsVqamlUDeBnd6JbNYa1UWC/npkNzEHqM5saL3vk/nNorqdfjYwdcdmhLtYbnuwVy+3/Q=="], + + "@next/env": ["@next/env@16.2.4", "", {}, "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "agora-agent-client-toolkit": ["agora-agent-client-toolkit@1.2.0", "", { "optionalDependencies": { "@agora-js/report": ">=4.19.0", "jszip": ">=3.0.0" }, "peerDependencies": { "agora-rtc-sdk-ng": ">=4.23.4", "agora-rtm": ">=2.0.0" }, "optionalPeers": ["agora-rtm"] }, "sha512-rbSDfk6veGsvNxSgccPa1zCCJ7QQw0NjoETHiHSXGURkzw90KwOQ2ZIOX6dueMFvZLUBr1UPQodCpOHroHY5bw=="], + + "agora-agent-server-sdk": ["agora-agent-server-sdk@1.3.2", "", { "dependencies": { "agora-token": "^2.0.5" } }, "sha512-rIQBtF5umlLeE8j9T2Wq2DapdkECb1ERTJtzdrriiae9daAjn/cJS6R6wJ2N1lfnjZhjnViBuRlkqpUI02pgOw=="], + + "agora-agent-uikit": ["agora-agent-uikit@1.1.0", "", { "dependencies": { "@lottiefiles/dotlottie-react": "^0.17.13", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "dompurify": "^3.3.3", "lucide-react": ">=0.263.0", "tailwind-merge": "^3.4.0" }, "peerDependencies": { "agora-agent-client-toolkit": "^1.2.0", "agora-agent-client-toolkit-react": "^1.2.0", "agora-rtc-react": ">=2.0.0", "agora-rtm-sdk": ">=2.0.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "optionalPeers": ["agora-agent-client-toolkit", "agora-agent-client-toolkit-react", "agora-rtc-react", "agora-rtm-sdk"] }, "sha512-gDSleHFYavNEqf/SIUt8xA1Q+VAw4M9q/84MD/+fFG7Z15oSLH6hZnmI+8cr6yzPXBBXHX4dd9tRZod5UjaFyA=="], + + "agora-conversational-ai-demo": ["agora-conversational-ai-demo@workspace:web-client"], + + "agora-rtc-react": ["agora-rtc-react@2.5.1", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-ORPbV02p8dZ2GhQxo5kTarc0BYjObc2IeqRPv9mi5/fxDI5nMY6kg54R5fUvLQU05H3Q5Kecq7M2kuZzHfNo5Q=="], + + "agora-rtc-sdk-ng": ["agora-rtc-sdk-ng@4.24.3", "", { "dependencies": { "@agora-js/media": "4.24.3", "@agora-js/protocol": "4.24.3", "@agora-js/report": "4.24.3", "@agora-js/shared": "4.24.3", "agora-rte-extension": "^1.2.4", "axios": "^1.13.6", "formdata-polyfill": "^4.0.7", "pako": "^2.1.0", "ua-parser-js": "^0.7.34", "webrtc-adapter": "8.2.0" } }, "sha512-DEz0+/jgsajZgWFG/uHijoMeheqr7YWUJ5AyubiqKt5vC1TF0/3v51vVl2aqupY+YkmJOg0q64B4AJcdpj/TBQ=="], + + "agora-rte-extension": ["agora-rte-extension@1.2.4", "", {}, "sha512-0ovZz1lbe30QraG1cU+ji7EnQ8aUu+Hf3F+a8xPml3wPOyUQEK6CTdxV9kMecr9t+fIDrGeW7wgJTsM1DQE7Nw=="], + + "agora-rtm": ["agora-rtm@2.2.4", "", { "peerDependencies": { "agora-rtc-sdk-ng": "4.23.0" } }, "sha512-c2BPYiIjAF7I8yMjplD+w8o0SieVGiuIDN0YSOT+Xg7GpGmK60F3IPzHMuWICMpmThcf3CS8NdmxXHIBh62+ug=="], + + "agora-token": ["agora-token@2.0.5", "", { "dependencies": { "crc-32": "^1.2.0", "cuint": "0.2.2", "md5": "^2.3.0" } }, "sha512-0NcbzC3iuutlksv3b4bCMKHrW3pko6gdiGEMRo6APDice24kfXAuWyAlG9hRBrrPBVDShwm9/GUz2Scd3zuZQw=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="], + + "axios": ["axios@1.15.2", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001790", "", {}, "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "concurrently": ["concurrently@8.2.2", "", { "dependencies": { "chalk": "^4.1.2", "date-fns": "^2.30.0", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", "spawn-command": "0.0.2", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], + + "crypt": ["crypt@0.0.2", "", {}, "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "cuint": ["cuint@0.2.2", "", {}, "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw=="], + "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dompurify": ["dompurify@3.4.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.344", "", {}, "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "lucide-react": ["lucide-react@1.9.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6qVAmbgCjcJz7sAGSPSSJ++RAwjlK2XCbRrZKv63Ciko1KT8jX0//CXxgI3jg2HlJu8tADqdYlNDebmYjeoruA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "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" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + + "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "sdp": ["sdp@3.2.2", "", {}, "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "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" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "spawn-command": ["spawn-command@0.0.2", "", {}, "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], + + "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "webrtc-adapter": ["webrtc-adapter@8.2.0", "", { "dependencies": { "sdp": "^3.0.2" } }, "sha512-umxCMgedPAVq4Pe/jl3xmelLXLn4XZWFEMR5Iipb5wJ+k1xMX0yC4ZY9CueZUU1MjapFxai1tFGE7R/kotH6Ww=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -65,6 +598,30 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "jszip/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], } } diff --git a/package.json b/package.json index 098d8d0..51ac1d8 100644 --- a/package.json +++ b/package.json @@ -2,23 +2,36 @@ "name": "agora-conversational-ai-demo", "version": "1.0.0", "private": true, + "workspaces": [ + "web-client" + ], "scripts": { "dev": "bun run dev:check && concurrently -n backend,frontend -c blue,green \"bun run dev:backend\" \"bun run dev:frontend\"", "dev:check": "bun run setup:env && bun run setup:deps", "dev:backend": "cd server-python && bash -c '(test -d venv || python3 -m venv venv) && source venv/bin/activate && pip install -q -r requirements.txt && python src/server.py'", - "dev:frontend": "cd web-client && bun run dev", + "dev:frontend": "cd web-client && AGENT_BACKEND_URL=http://localhost:8000 bun run dev", "backend": "cd server-python && source venv/bin/activate && python3 src/server.py", - "frontend": "cd web-client && bun run dev", + "frontend": "cd web-client && AGENT_BACKEND_URL=http://localhost:8000 bun run dev", "setup": "bun run setup:env && bun run setup:backend && bun run setup:frontend && bun run setup:done", - "setup:env": "test -f server-python/.env.local || (cp server-python/.env.example server-python/.env.local && echo '\n⚠️ Created server-python/.env.local - Please edit your Agora credentials.' && echo ' Required: APP_ID and APP_CERTIFICATE. Managed provider access must be enabled for this project.\n')", - "setup:deps": "test -d web-client/node_modules || (echo 'Installing frontend dependencies...' && cd web-client && bun install)", + "setup:env": "test -f server-python/.env.local || (echo '\n⚠️ Missing server-python/.env.local.' && echo ' Recommended: agora login && agora project create my-first-voice-agent --feature rtc --feature convoai && agora project use my-first-voice-agent && agora project env write server-python/.env.local --with-secrets' && echo ' Reference template: server-python/.env.example\n' && exit 1)", + "setup:deps": "test -d node_modules || (echo 'Installing workspace dependencies...' && bun install)", "setup:backend": "cd server-python && python3 -m venv venv && source venv/bin/activate && pip install --upgrade pip && PIP_INDEX_URL=https://pypi.org/simple pip install -r requirements.txt", - "setup:frontend": "cd web-client && bun install", - "setup:done": "echo '\n✅ Setup complete! Next steps:' && echo ' 1. Edit server-python/.env.local with APP_ID and APP_CERTIFICATE' && echo ' 2. Run: bun run dev\n'", + "setup:frontend": "bun install", + "setup:done": "echo '\n✅ Setup complete! Next steps:' && echo ' 1. Ensure server-python/.env.local exists (preferred: agora project env write server-python/.env.local --with-secrets)' && echo ' 2. Run: bun run dev\n'", + "doctor": "bash -c 'set -e; echo \"Checking shared repo prerequisites...\"; command -v bun >/dev/null && echo \"- bun available\" || { echo \"- bun not found\"; exit 1; }; test -d node_modules && echo \"- workspace dependencies installed\" || { echo \"- root node_modules missing; run bun install\"; exit 1; }'", + "doctor:local": "bash -c 'set -e; bun run doctor; command -v python3 >/dev/null && echo \"- python3 available\" || { echo \"- python3 not found\"; exit 1; }; test -f server-python/.env.local && echo \"- server-python/.env.local present\" || { echo \"- missing server-python/.env.local\"; exit 1; }; grep -Eq \"^AGORA_APP_ID=.+$\" server-python/.env.local && echo \"- AGORA_APP_ID configured\" || { echo \"- AGORA_APP_ID missing in server-python/.env.local\"; exit 1; }; grep -Eq \"^AGORA_APP_CERTIFICATE=.+$\" server-python/.env.local && echo \"- AGORA_APP_CERTIFICATE configured\" || { echo \"- AGORA_APP_CERTIFICATE missing in server-python/.env.local\"; exit 1; }'", "build": "cd web-client && bun run build", + "verify": "bun run verify:web", + "verify:local": "bun run doctor:local && bun run verify:backend && bun run verify:local:fastapi && bun run verify:web:proxy && bun run verify:web:build", + "verify:local:fastapi": "cd web-client && bun run scripts/verify-local-fastapi.ts", + "verify:backend": "cd server-python && python3 -m py_compile src/server.py src/agent.py", + "verify:web": "bun run doctor && bun run verify:web:api && bun run verify:web:build", + "verify:web:api": "cd web-client && bun run scripts/verify-api-contracts.ts", + "verify:web:proxy": "cd web-client && bun run scripts/verify-local-proxy.ts", + "verify:web:build": "cd web-client && bun run build", "clean": "bun run clean:backend && bun run clean:frontend", "clean:backend": "rm -rf server-python/venv server-python/__pycache__ server-python/src/__pycache__", - "clean:frontend": "rm -rf web-client/node_modules web-client/dist" + "clean:frontend": "rm -rf node_modules web-client/node_modules web-client/.next web-client/dist" }, "devDependencies": { "concurrently": "^8.2.2" diff --git a/server-python/.env.example b/server-python/.env.example index a4f6dc8..7360cd5 100644 --- a/server-python/.env.example +++ b/server-python/.env.example @@ -1,14 +1,4 @@ -# Agora Configuration -APP_ID=your_agora_app_id -APP_CERTIFICATE=your_agora_app_certificate - -# Optional BYOK examples: -# - Replace the matching blocks in src/agent.py if you want to use -# - your own vendor credentials instead of the default managed preset path. -# DEEPGRAM_API_KEY= -# OPENAI_API_KEY= -# ELEVENLABS_API_KEY= -# ELEVENLABS_VOICE_ID=pNInz6obpgDQGcFmaJgB - -# Server Configuration +AGORA_APP_ID=your_agora_app_id +AGORA_APP_CERTIFICATE=your_agora_app_certificate +AGENT_GREETING=Hi there! I'm Ada, your virtual assistant from Agora. How can I help? PORT=8000 diff --git a/server-python/AGENTS.md b/server-python/AGENTS.md index 90a8274..8217af1 100644 --- a/server-python/AGENTS.md +++ b/server-python/AGENTS.md @@ -1,194 +1,83 @@ -# Agora Agent Service - AI Assistant Guide +# Python Backend Agent Guide -This document is designed for AI programming assistants to understand and work with this project effectively. +Use this guide when changing files under `server-python/`. -## Project Overview +## Current Role -**Purpose:** FastAPI-based service for managing Agora Conversational AI Agents +This module is the local FastAPI backend for the quickstart. It remains the authoritative local backend when developing the full stack on one machine. -**Tech Stack:** -- Python 3.8+ -- FastAPI (web framework) -- agora-agent-server-sdk (Agora Agent SDK) -- uvicorn (ASGI server) - -**Architecture:** -``` -HTTP Request → FastAPI (server.py) → Agent (agent.py) → agora-agent-server-sdk → Agora API -``` +The deployed web app can also serve `/api/*` directly from Next route handlers, so do not assume Python owns production traffic in every environment. -**Key Components:** -- `src/server.py` - HTTP endpoints and request handling -- `src/agent.py` - Business logic wrapper around SDK -- SDK handles token generation, API calls, and configuration +## Current Stack -## Build and Test Commands - -### Setup -```bash -# First time setup -cp .env.example .env.local -# Edit .env.local with actual API keys +- Python 3.8+ +- FastAPI +- `agora-agent-server-sdk` +- `python-dotenv` +- `uvicorn` -# Install dependencies -pip install -r requirements.txt -``` +## Current Implementation Model -### Run -```bash -# Development (simple) -python src/server.py +- `src/server.py` exposes `/get_config`, `/v2/startAgent`, and `/v2/stopAgent` +- `src/agent.py` wraps `AsyncAgora` +- agent sessions are scoped to the requesting user with `remote_uids=[user_uid]` +- stop is idempotent through a session stop first, then `client.stop_agent(...)` fallback +- token expiry is 1 hour +- default providers are the managed Deepgram STT, OpenAI LLM, and MiniMax TTS path used by the current quickstart -# Development (auto-reload) -uvicorn src.server:app --host 0.0.0.0 --port 8000 --reload +## Environment -# Production -gunicorn src.server:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 -``` +Setup from the repo root: -### Test ```bash -# Test config generation -curl http://localhost:8000/get_config - -# Test agent start -curl -X POST http://localhost:8000/v2/startAgent \ - -H "Content-Type: application/json" \ - -d '{"channelName": "test", "rtcUid": "123", "userUid": "456"}' - -# Test agent stop -curl -X POST http://localhost:8000/v2/stopAgent \ - -H "Content-Type: application/json" \ - -d '{"agentId": "agent_id"}' +cp server-python/.env.example server-python/.env.local ``` -## Code Style Guidelines +Required values: -### Python Conventions -- **Style:** PEP 8 -- **Type hints:** Required for all function parameters and return values -- **Imports:** Absolute imports preferred, group by standard/third-party/local -- **Line length:** 100 characters max +```bash +AGORA_APP_ID=your_agora_app_id +AGORA_APP_CERTIFICATE=your_agora_app_certificate +``` -### Naming -- Functions/variables: `snake_case` -- Classes: `PascalCase` -- Constants: `UPPER_SNAKE_CASE` -- Private: prefix with `_` +Optional values: -### Comments -- Keep docstrings concise and natural -- Avoid obvious comments -- Focus on "why" not "what" +```bash +PORT=8000 +AGENT_GREETING=Custom greeting +``` -## Security Considerations +Do not assume separate ASR, LLM, or TTS vendor secrets are required unless the code introduces a custom non-managed provider path. -### Environment Variables -- **Never commit** `.env.local` or actual API keys -- Use `.env.example` as template only -- All secrets must be loaded from environment +## Important Files -### API Keys Required -- `APP_ID`, `APP_CERTIFICATE` - Agora credentials (required) -- `LLM_API_KEY` - LLM API key (required) -- `TTS_ELEVENLABS_API_KEY` - ElevenLabs API key (required) -- `ASR_DEEPGRAM_API_KEY` - Deepgram API key (required) +- `src/server.py`: FastAPI routes and config generation +- `src/agent.py`: async agent lifecycle and provider configuration +- `.env.example`: local env template +- `README.md`: backend-specific setup and API examples -## Project Structure +## Commands -``` -server-python/ -├── src/ -│ ├── server.py # FastAPI app, HTTP endpoints -│ └── agent.py # Agent lifecycle management -├── .env.example # Environment template (safe to commit) -├── .env.local # Actual secrets (never commit) -├── requirements.txt # Python dependencies -├── README.md # User documentation -└── AGENTS.md # This file (AI assistant guide) -``` +From the repo root: -## Common Patterns - -### Agent Configuration Pattern -```python -from agora_agent import Agora, Area -from agora_agent.agentkit import Agent as AgoraAgent -from agora_agent.agentkit.vendors import DeepgramSTT, MiniMaxTTS, OpenAI - -# Create Agora client (Token007 auth from APP_ID + APP_CERTIFICATE) -client = Agora(area=Area.US, app_id=app_id, app_certificate=app_certificate) - -# Create agent with fluent API -agora_agent = AgoraAgent( - name="agent_name", - instructions="System prompt", - greeting="Hello message", - advanced_features={"enable_rtm": True}, - parameters={"data_channel": "rtm", "enable_error_message": True}, -) - -agora_agent = ( - agora_agent - .with_llm(OpenAI(model="gpt-4o-mini")) - .with_tts(MiniMaxTTS(model="speech_2_6_turbo", voice_id="English_captivating_female1")) - .with_stt(DeepgramSTT(model="nova-3", language="en")) -) - -session = agora_agent.create_session( - client=client, - channel=channel, - agent_uid=agent_uid, - remote_uids=[user_uid], # Subscribe only the requester - enable_string_uid=True, - idle_timeout=30, - expires_in=3600, -) -agent_id = session.start() +```bash +bun run backend +bun run doctor:local +bun run verify:backend ``` -### Error Handling Pattern -```python -try: - result = agent.some_method() - return {"code": 0, "msg": "success", "data": result} -except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) -except RuntimeError as e: - raise HTTPException(status_code=500, detail=str(e)) -``` +From `server-python/` directly: -### Token Generation -```python -from agora_agent.agentkit.token import generate_convo_ai_token - -token = generate_convo_ai_token( - app_id=app_id, - app_certificate=app_certificate, - channel_name=channel_name, - account=str(user_uid), - token_expire=86400, -) +```bash +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +uvicorn src.server:app --host 0.0.0.0 --port 8000 --reload ``` -## Dependencies - -### Core -- `fastapi>=0.100.0` - Web framework -- `uvicorn>=0.20.0` - ASGI server -- `agora-agent-server-sdk` - Agora Agent SDK -- `python-dotenv>=1.0.0` - Environment management - -## Troubleshooting - -### Import Errors -**Symptom:** `ModuleNotFoundError: No module named 'agora_agent'` -**Solution:** Ensure `agora-agent-server-sdk` is installed: `pip install -r requirements.txt` - -### Configuration Errors -**Symptom:** Service fails to start with ValueError -**Solution:** Check `.env.local` exists and contains all required variables +## Working Rules -### Port Conflicts -**Symptom:** `address already in use` -**Solution:** Change `PORT` in `.env.local` or kill existing process with `lsof -ti :8000 | xargs kill -9` +- Keep the FastAPI handlers async-friendly. +- Keep token generation behavior aligned with the Next route handlers. +- If you change the request or response contract, update the web client and root README in the same change. +- If you change agent defaults, update both backend implementations or document the intended divergence clearly. diff --git a/server-python/ARCHITECTURE.md b/server-python/ARCHITECTURE.md index ae0d940..a383151 100644 --- a/server-python/ARCHITECTURE.md +++ b/server-python/ARCHITECTURE.md @@ -2,12 +2,14 @@ ## Overview -Python FastAPI service providing REST APIs for Agora Conversational AI Agent management and token generation. +Python FastAPI service providing the local backend path for token generation and Agora agent management. -**Core Responsibilities**: +**Core Responsibilities in local development**: - Generate RTC/RTM tokens for client connections - Start/stop AI agents with ASR, LLM, and TTS configuration -- Proxy between frontend and Agora Conversational AI API +- Provide a stateless-safe FastAPI bridge between the frontend and Agora Conversational AI APIs + +In deployed web mode, the Next app can serve the same API contract directly. This module is therefore the local development backend, not the only backend implementation in the repo. ## Tech Stack @@ -59,30 +61,32 @@ agent = Agent() # Singleton **Responsibilities**: - Wrap agora-agent-server-sdk - Configure ASR/LLM/TTS providers -- Manage agent lifecycle (start/stop via AgentSession) +- Manage agent lifecycle with async session start and stateless-safe stop fallback - Parameter validation **Key Components**: ```python -from agora_agent import Agora, Area +from agora_agent import Area, AsyncAgora from agora_agent.agentkit import Agent as AgoraAgent from agora_agent.agentkit.vendors import DeepgramSTT, MiniMaxTTS, OpenAI class Agent: def __init__(self): - self.client = Agora( - area=Area.CN, + self.client = AsyncAgora( + area=Area.US, app_id=self.app_id, app_certificate=self.app_certificate, ) - def start(channel_name, agent_uid, user_uid): + async def start(channel_name, agent_uid, user_uid): agora_agent = AgoraAgent( name=name, - instructions="...", - greeting="...", - advanced_features={"enable_rtm": True}, + instructions="Ada persona prompt...", + greeting="Hi there! I'm Ada, your virtual assistant from Agora. How can I help?", + max_history=50, + turn_detection={...}, + advanced_features={"enable_rtm": True, "enable_tools": True}, parameters={"data_channel": "rtm", "enable_error_message": True}, ) agora_agent = ( @@ -100,7 +104,7 @@ class Agent: idle_timeout=30, expires_in=3600, ) - return session.start() # returns agent_id + return await session.start() # returns agent_id ``` ## API Endpoints @@ -127,7 +131,7 @@ Generate connection configuration for frontend client. **Logic**: 1. Generate random UIDs for user and agent 2. Create channel name with timestamp -3. Generate token via `generate_convo_ai_token()` (24h expiry) +3. Generate token via `generate_convo_ai_token()` (1h expiry) 4. Return configuration bundle ### POST /v2/startAgent @@ -138,8 +142,8 @@ Start an AI agent in specified channel. ```json { "channelName": "channel_1770199765", - "rtcUid": "58888506", - "userUid": "1234567" + "rtcUid": 58888506, + "userUid": 1234567 } ``` @@ -182,15 +186,13 @@ Stop a running agent. Loaded from `.env.local` (priority) or `.env`: ```bash -APP_ID=your_app_id # Agora App ID -APP_CERTIFICATE=your_app_certificate # Agora App Certificate -ASR_DEEPGRAM_API_KEY=your_key # Speech-to-Text -LLM_API_KEY=your_key # Language Model -TTS_ELEVENLABS_API_KEY=your_key # Text-to-Speech -PORT=8000 # HTTP server port +AGORA_APP_ID=your_app_id +AGORA_APP_CERTIFICATE=your_app_certificate +AGENT_GREETING=Hi there! I'm Ada, your virtual assistant from Agora. How can I help? +PORT=8000 ``` -**Note**: Uses Token007 authentication generated from `APP_ID` and `APP_CERTIFICATE`. No API_KEY/API_SECRET needed. +**Note**: Uses Token007 authentication generated from `AGORA_APP_ID` and `AGORA_APP_CERTIFICATE`. No third-party vendor keys are required in the default managed setup. ### Token Generation @@ -202,7 +204,7 @@ token = generate_convo_ai_token( app_certificate=app_certificate, channel_name=channel_name, account=str(user_uid), - token_expire=86400, + token_expire=3600, ) ``` @@ -235,7 +237,7 @@ FastAPI Router (server.py) ↓ Agent Class (agent.py) ↓ -agora-agent-server-sdk (AgentSession) +agora-agent-server-sdk (AsyncAgora + AgentSession) ↓ Agora REST API ↓ @@ -248,12 +250,12 @@ Frontend Client ## Integration with Frontend -Frontend connects via Next.js proxy (`proxy.ts`): +Frontend connects through Next route handlers in `web-client/app/api`. In local Python mode, those handlers forward to the FastAPI service through `AGENT_BACKEND_URL`: ``` -/api/get_config → http://localhost:8000/get_config -/api/v2/startAgent → http://localhost:8000/v2/startAgent -/api/v2/stopAgent → http://localhost:8000/v2/stopAgent +/api/get_config → Next route handler → http://localhost:8000/get_config +/api/v2/startAgent → Next route handler → http://localhost:8000/v2/startAgent +/api/v2/stopAgent → Next route handler → http://localhost:8000/v2/stopAgent ``` ## Error Handling @@ -262,7 +264,7 @@ Frontend connects via Next.js proxy (`proxy.ts`): |-----------|-------------|-------------| | ValueError | 400 | Invalid parameters | | RuntimeError | 500 | SDK/API errors | -| Agent not found | 404 | Invalid agent_id | +| Agent not found | 200 | Stop is treated as idempotent when the platform session is already gone | ## Dependencies diff --git a/server-python/README.md b/server-python/README.md index 56016d0..60331a9 100644 --- a/server-python/README.md +++ b/server-python/README.md @@ -3,20 +3,36 @@ Agora Conversational AI Agent service built with FastAPI. ## Quick Start + +Use the repo-root [README.md](../README.md) for the normal full-stack local flow. This document is for working on the Python backend module directly. + Follow [Get started with Agora](https://docs.agora.io/en/conversational-ai/get-started/manage-agora-account#enable-conversational-ai) to get the **App ID** and **App Certificate** and enable the **Conversational AI** service. +From `server-python/`: + ### 1. Configure Environment +Preferred: + +```bash +agora login +agora project create my-first-voice-agent --feature rtc --feature convoai +agora project use my-first-voice-agent +agora project env write .env.local --with-secrets +``` + +Reference fallback: + ```bash cp .env.example .env.local ``` -Edit `.env.local` and fill in your Agora credentials: -- `APP_ID` - Your Agora App ID (Required) -- `APP_CERTIFICATE` - Your Agora App Certificate (Required) +`.env.example` is the reference template. If you are not using the Agora CLI, edit `.env.local` and fill in your Agora credentials: +- `AGORA_APP_ID` - Your Agora App ID (Required) +- `AGORA_APP_CERTIFICATE` - Your Agora App Certificate (Required) - Agora managed provider access should be enabled for this project -**Note**: The service uses Token007 authentication generated from `APP_ID` and `APP_CERTIFICATE`. Third-party vendor keys are not required in this default managed setup. The current default chain matches the Next.js quickstart: `DeepgramSTT` (`nova-3`) + `OpenAI` (`gpt-4o-mini`) + `MiniMaxTTS` (`speech_2_6_turbo` / `English_captivating_female1`). +**Note**: The service uses Token007 authentication generated from `AGORA_APP_ID` and `AGORA_APP_CERTIFICATE`. Third-party vendor keys are not required in this default managed setup. The current default chain matches the Next.js quickstart: `DeepgramSTT` (`nova-3`) + `OpenAI` (`gpt-4o-mini`) + `MiniMaxTTS` (`speech_2_6_turbo` / `English_captivating_female1`). The FastAPI sample now uses `AsyncAgora` so the request path matches the local Agora guidance for async frameworks. ### 2. Install Dependencies @@ -41,6 +57,12 @@ python src/server.py The service will start on port 8000 (or the port specified in `.env.local`). +## How This Fits The Repo + +- Full-stack local development: run `bun run dev` from the repo root. The browser still calls Next `/api/*`, and those route handlers proxy to this FastAPI service. +- Module-local backend work: use the commands in this README when you only need to run or inspect the Python service itself. +- Single-target web deployment: this Python service is not required unless you intentionally point `AGENT_BACKEND_URL` at an external backend. + ### 4. Test API ```bash @@ -50,7 +72,7 @@ curl http://localhost:8000/get_config # Test agent start curl -X POST http://localhost:8000/v2/startAgent \ -H "Content-Type: application/json" \ - -d '{"channelName": "test_channel", "rtcUid": "123456", "userUid": "789012"}' + -d '{"channelName": "test_channel", "rtcUid": 123456, "userUid": 789012}' # Test agent stop (use agent_id from start response) curl -X POST http://localhost:8000/v2/stopAgent \ @@ -64,6 +86,10 @@ curl -X POST http://localhost:8000/v2/stopAgent \ - `POST /v2/startAgent` - Start an agent - `POST /v2/stopAgent` - Stop an agent +`/get_config` now issues one-hour RTC plus RTM tokens. The web client renews both before expiry, matching the reference Next.js session model. + +The repo-level `bun run verify:local:fastapi` check exercises this FastAPI app through the Next proxy path, but it swaps in a fake agent implementation so route wiring can be verified without depending on a live agent start. + ## Requirements - Python >= 3.8 diff --git a/server-python/scripts/run_fake_server.py b/server-python/scripts/run_fake_server.py new file mode 100644 index 0000000..0cf8d04 --- /dev/null +++ b/server-python/scripts/run_fake_server.py @@ -0,0 +1,44 @@ +import os +import sys + +import uvicorn + + +class FakeAgent: + def __init__(self): + self.started_agent_ids = set() + + async def start(self, channel_name: str, agent_uid: int, user_uid: int): + if not channel_name or agent_uid <= 0 or user_uid <= 0: + raise ValueError("channel_name, agent_uid, and user_uid must be valid") + + agent_id = f"fake-agent-{agent_uid}" + self.started_agent_ids.add(agent_id) + return { + "agent_id": agent_id, + "channel_name": channel_name, + "status": "started", + } + + async def stop(self, agent_id: str): + if not agent_id: + raise ValueError("agent_id is required") + self.started_agent_ids.discard(agent_id) + + +def main(): + server_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + src_root = os.path.join(server_root, "src") + if src_root not in sys.path: + sys.path.insert(0, src_root) + + import server as server_module + + server_module.agent = FakeAgent() + + port = int(os.getenv("PORT", "8000")) + uvicorn.run(server_module.app, host="127.0.0.1", port=port) + + +if __name__ == "__main__": + main() diff --git a/server-python/src/agent.py b/server-python/src/agent.py index 252dc17..bb070e0 100644 --- a/server-python/src/agent.py +++ b/server-python/src/agent.py @@ -6,10 +6,18 @@ import os import time from typing import Any, Dict -from agora_agent import Agora, Area + +from agora_agent import Area, AsyncAgora from agora_agent.agentkit import Agent as AgoraAgent from agora_agent.agentkit.vendors import DeepgramSTT, MiniMaxTTS, OpenAI +ADA_PROMPT = """You are Ada, an agentic developer advocate from Agora. You help developers understand and build with Agora's Conversational AI platform. + +Agora is a real-time communications company. The product you represent is the Agora Conversational AI Engine. + +If you do not know a specific fact about Agora, say so plainly and suggest checking docs.agora.io. Keep most replies to one or two sentences unless the user explicitly asks for more detail. +""" + class Agent: """ @@ -20,33 +28,37 @@ class Agent: """ def __init__(self): - self.app_id = os.getenv("APP_ID") - self.app_certificate = os.getenv("APP_CERTIFICATE") - + self.app_id = os.getenv("AGORA_APP_ID") + self.app_certificate = os.getenv("AGORA_APP_CERTIFICATE") + self.greeting = os.getenv( + "AGENT_GREETING", + "Hi there! I'm Ada, your virtual assistant from Agora. How can I help?", + ) + if not self.app_id or not self.app_certificate: - raise ValueError("APP_ID and APP_CERTIFICATE are required") - - self.client = Agora( + raise ValueError("AGORA_APP_ID and AGORA_APP_CERTIFICATE are required") + + self.client = AsyncAgora( area=Area.US, app_id=self.app_id, app_certificate=self.app_certificate, ) - + # Track active sessions by agent_id self._sessions: Dict[str, Any] = {} - - def start( + + async def start( self, channel_name: str, - agent_uid: str, - user_uid: str + agent_uid: int, + user_uid: int ) -> Dict[str, Any]: """Start agent with the same default vendor chain as the Next.js quickstart.""" if not channel_name or not str(channel_name).strip(): raise ValueError("channel_name is required and cannot be empty") - if not agent_uid or not str(agent_uid).strip(): + if agent_uid <= 0: raise ValueError("agent_uid is required and cannot be empty") - if not user_uid or not str(user_uid).strip(): + if user_uid <= 0: raise ValueError("user_uid is required and cannot be empty") name = f"agent_{channel_name}_{agent_uid}_{int(time.time())}" @@ -54,8 +66,8 @@ def start( # Default managed path: DeepgramSTT + OpenAI + MiniMaxTTS. llm = OpenAI( model="gpt-4o-mini", - greeting_message="Hello! I am your AI assistant. How can I help you?", - failure_message="I'm sorry, I'm having trouble processing your request.", + greeting_message=self.greeting, + failure_message="Please wait a moment.", max_history=15, max_tokens=1024, temperature=0.7, @@ -86,13 +98,32 @@ def start( # model_id="eleven_flash_v2_5", # voice_id=os.getenv("ELEVENLABS_VOICE_ID", "pNInz6obpgDQGcFmaJgB"), # ) - + agora_agent = AgoraAgent( name=name, - instructions="You are a helpful AI assistant.", - greeting="Hello! I am your AI assistant. How can I help you?", - failure_message="I'm sorry, I'm having trouble processing your request.", - advanced_features={"enable_rtm": True}, + instructions=ADA_PROMPT, + greeting=self.greeting, + failure_message="Please wait a moment.", + max_history=50, + turn_detection={ + "config": { + "speech_threshold": 0.5, + "start_of_speech": { + "mode": "vad", + "vad_config": { + "interrupt_duration_ms": 160, + "prefix_padding_ms": 300, + }, + }, + "end_of_speech": { + "mode": "vad", + "vad_config": { + "silence_duration_ms": 480, + }, + }, + }, + }, + advanced_features={"enable_rtm": True, "enable_tools": True}, parameters={"data_channel": "rtm", "enable_error_message": True}, ) @@ -107,14 +138,14 @@ def start( client=self.client, channel=channel_name, agent_uid=str(agent_uid), - remote_uids=["*"], + remote_uids=[str(user_uid)], enable_string_uid=True, idle_timeout=30, expires_in=3600, ) - agent_id = session.start() - + agent_id = await session.start() + # Save session for later stop self._sessions[agent_id] = session @@ -123,14 +154,19 @@ def start( "channel_name": channel_name, "status": "started", } - - def stop(self, agent_id: str) -> None: - """Stop a running agent via its session.""" + + async def stop(self, agent_id: str) -> None: + """Stop a running agent. Falls back to the stateless client path.""" if not agent_id or not str(agent_id).strip(): raise ValueError("agent_id is required and cannot be empty") - + session = self._sessions.pop(agent_id, None) if session: - session.stop() - else: - raise ValueError(f"No active session found for agent_id: {agent_id}") + try: + await session.stop() + return + except Exception: + # Fall back to the stateless SDK path if the in-memory session is stale. + pass + + await self.client.stop_agent(agent_id) diff --git a/server-python/src/server.py b/server-python/src/server.py index de98b34..725208a 100644 --- a/server-python/src/server.py +++ b/server-python/src/server.py @@ -10,6 +10,7 @@ import os import random import time +from typing import Optional from dotenv import load_dotenv # Load environment variables from .env.local or .env @@ -17,7 +18,7 @@ load_dotenv(os.path.join(_base_dir, '.env.local')) load_dotenv(os.path.join(_base_dir, '.env')) -from fastapi import FastAPI, APIRouter, HTTPException +from fastapi import APIRouter, FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from agora_agent.agentkit.token import generate_convo_ai_token @@ -61,8 +62,8 @@ def _to_http_error(exc: Exception) -> HTTPException: class StartAgentRequest(BaseModel): """Request body for POST /v2/startAgent""" channelName: str - rtcUid: str - userUid: str + rtcUid: int + userUid: int class StopAgentRequest(BaseModel): @@ -71,8 +72,15 @@ class StopAgentRequest(BaseModel): # API endpoints +def _generate_channel_name() -> str: + return f"ai-conversation-{int(time.time())}-{random.randint(1000, 9999)}" + + @router.get("/get_config") -def get_config(): +async def get_config( + channel: Optional[str] = Query(default=None), + uid: Optional[int] = Query(default=None), +): """Generate connection configuration""" if agent is None: raise HTTPException( @@ -81,34 +89,31 @@ def get_config(): ) try: - # Generate random UIDs - user_uid = random.randint(1000, 9999999) - agent_uid = random.randint(10000000, 99999999) - - # Generate channel name - channel_name = f"channel_{int(time.time())}" - + user_uid = uid or random.randint(1000, 9999999) + agent_uid = str(random.randint(10000000, 99999999)) + channel_name = channel or _generate_channel_name() + # Get credentials from environment - app_id = os.getenv("APP_ID") - app_certificate = os.getenv("APP_CERTIFICATE") - - # Generate convo AI token (RTC + RTM) + app_id = os.getenv("AGORA_APP_ID") + app_certificate = os.getenv("AGORA_APP_CERTIFICATE") + + # Generate a one-hour RTC+RTM token and renew it client-side as needed. token = generate_convo_ai_token( app_id=app_id, app_certificate=app_certificate, channel_name=channel_name, account=str(user_uid), - token_expire=86400, + token_expire=3600, ) - + config_data = { "app_id": app_id, "token": token, "uid": str(user_uid), "channel_name": channel_name, - "agent_uid": str(agent_uid) + "agent_uid": agent_uid, } - + return { "code": 0, "data": config_data, @@ -119,7 +124,7 @@ def get_config(): @router.post("/v2/startAgent") -def start_agent(request: StartAgentRequest): +async def start_agent(request: StartAgentRequest): """Start agent in a channel""" if agent is None: raise HTTPException( @@ -128,7 +133,7 @@ def start_agent(request: StartAgentRequest): ) try: - result = agent.start( + result = await agent.start( channel_name=request.channelName, agent_uid=request.rtcUid, user_uid=request.userUid, @@ -139,7 +144,7 @@ def start_agent(request: StartAgentRequest): @router.post("/v2/stopAgent") -def stop_agent(request: StopAgentRequest): +async def stop_agent(request: StopAgentRequest): """Stop agent by ID""" if agent is None: raise HTTPException( @@ -148,11 +153,9 @@ def stop_agent(request: StopAgentRequest): ) try: - agent.stop(request.agentId) + await agent.stop(request.agentId) return {"code": 0, "msg": "success"} except Exception as e: - if isinstance(e, ValueError): - raise HTTPException(status_code=404, detail=str(e)) raise _to_http_error(e) diff --git a/system-architecture-dark.svg b/system-architecture-dark.svg new file mode 100644 index 0000000..34f78af --- /dev/null +++ b/system-architecture-dark.svg @@ -0,0 +1,51 @@ + + + + + Local Python-Backed Development + Single-Target Web Deployment + + + Browser + RTC + RTM client + + + Next.js App + /api/* route handlers + :3000 + + + FastAPI Backend + Proxy target via AGENT_BACKEND_URL + :8000 + + + Agora + Cloud + + + Browser + RTC + RTM client + + + Next.js App + UI + in-process token/agent routes + + + Agora Cloud + RTC, RTM, agent runtime + + + + + + + + + + + + + + Local mode keeps the browser-facing /api contract in Next.js and forwards backend work to FastAPI. + diff --git a/system-architecture.svg b/system-architecture.svg new file mode 100644 index 0000000..b244535 --- /dev/null +++ b/system-architecture.svg @@ -0,0 +1,51 @@ + + + + + Local Python-Backed Development + Single-Target Web Deployment + + + Browser + RTC + RTM client + + + Next.js App + /api/* route handlers + :3000 + + + FastAPI Backend + Proxy target via AGENT_BACKEND_URL + :8000 + + + Agora + Cloud + + + Browser + RTC + RTM client + + + Next.js App + UI + in-process token/agent routes + + + Agora Cloud + RTC, RTM, agent runtime + + + + + + + + + + + + + + Local mode keeps the browser-facing /api contract in Next.js and forwards backend work to FastAPI. + diff --git a/web-client/.env.local.example b/web-client/.env.local.example index 4e7978a..8bf9635 100644 --- a/web-client/.env.local.example +++ b/web-client/.env.local.example @@ -1,2 +1,6 @@ -# Backend API URL (for development) -NEXT_PUBLIC_API_URL=http://localhost:8000 +AGORA_APP_ID=your_agora_app_id +AGORA_APP_CERTIFICATE=your_agora_app_certificate +AGENT_GREETING=Hi there! I'm Ada, your virtual assistant from Agora. How can I help? + +# Optional: point the Next API routes at the standalone Python backend in local development. +AGENT_BACKEND_URL=http://localhost:8000 diff --git a/web-client/AGENTS.md b/web-client/AGENTS.md index 0943520..8369247 100644 --- a/web-client/AGENTS.md +++ b/web-client/AGENTS.md @@ -1,296 +1,79 @@ - +# Web Client Agent Guide -# AGENTS.md +Use this guide when changing files under `web-client/`. -## Conversation Modes +## Current Stack -Identify mode based on user input and apply corresponding response strategy: +- Next.js 16 App Router +- React 19 +- TypeScript +- Tailwind CSS +- `agora-rtc-react` +- `agora-rtm` +- `agora-agent-client-toolkit` +- `agora-agent-uikit` -| Mode | Recognition | Strategy | -|------|-------------|----------| -| **workflow** | Contains feat/fix/refactor/chore, or explicit dev task | **Start workflow (mandatory state management)** | -| **continue** | "continue / resume", or unfinished PROJECT_STATE.md exists | Read state file, restore context, enter workflow | -| **general** | Technical questions, code explanations, general inquiries | Direct answer, no workflow | +## What This Module Owns ---- +- The landing screen and live conversation UI +- RTC client setup and channel join lifecycle +- RTM login, transcript handling, and token renewal +- Web-facing `/api/*` route handlers for quick deployment +- Optional local proxy fallback to the Python backend through `AGENT_BACKEND_URL` -## Mode Switching Rules +## Important Files -### general → workflow Fallback +- `app/page.tsx`: root page and Agora provider setup +- `app/api/**/route.ts`: server-side API handlers used in deployment and local proxy mode +- `src/components/app.tsx`: user-facing conversation experience +- `src/hooks/useAgoraConnection.ts`: RTC/RTM/agent lifecycle +- `src/lib/conversation.ts`: transcript helpers +- `src/lib/server/agora.ts`: server-side token and agent helpers +- `src/services/api.ts`: browser API client +- `next.config.ts`: Turbopack root configuration for the nested app -When **general mode response involves these operations**, must prompt user to switch to workflow mode: +## Request Flow -- File modifications (create, edit, delete) -- Command execution (git, build, test, etc.) -- Todo changes (add, update tasks) +### Local Development -**Prompt format**: +- Run `bun run dev` from the repo root +- The Next route handlers stay in the request path +- They proxy to `http://localhost:8000` when `AGENT_BACKEND_URL` is set by the root scripts -``` -⚠️ Development operation detected - -Current operation requires: -- [specific operation list] - -Recommend switching to workflow mode for state tracking. Switch? -``` - ---- - -## workflow Mode (Mandatory State Machine) - -### workflow Startup Gate (Hard Gate) - -> **Upon entering workflow mode, the following steps MUST be completed first. No dev actions allowed until done.** - -1. **Check `PROJECT_STATE.md`** - - Not exists → Create immediately (use `docs/PROJECT_STATE_TEMPLATE.md`) - - Exists → Check if update needed - -2. **Explicitly declare state result in current output (required)**: - -``` -[STATE] PROJECT_STATE.md: checked / updated -``` - -> ⚠️ **Missing `[STATE]` declaration = workflow not started.** - ---- - -### workflow Progress Display (Standard Output) - -``` -Entering workflow mode - -[🔍 Clarify] → [📐 Design] → [⚡ Execute] → [✅ Verify] → [📝 Summary] - ▲ Current -``` - ---- - -## Preferences - -- Language: English -- Timezone: UTC, YYYY-MM-DD, 24h - ---- - -## AI Behavior Standards - -### PROJECT_STATE.md Maintenance (Hard Constraint) - -**Core Principle**: -> **State maintenance is part of workflow, not optional behavior.** - -#### 1. Mandatory Check Per Turn (workflow mode) - -In workflow mode, **must complete before each response**: - -- Does PROJECT_STATE.md exist? -- Does current phase / todo / decisions need update? - -Provide **state anchor** in output, e.g.: - -``` -[STATE] PROJECT_STATE.md: todo 3 → 2, phase: 📐Design -``` - -**⚠️ Minimum Output Frequency Requirement**: - -> **Even if state unchanged, must include `[STATE]` line in every output.** - -Prevents "state fatigue" in long execution flows. - -#### 2. Mandatory Update Nodes (Cannot Skip) - -- Create / update todo -- Complete todo item -- After commit -- Phase transition -- Blockers / decision points -- **Even lightweight tasks (single file change, sync, tweak)** - -> ⚠️ **Cannot skip state update because "task too small".** - -#### 3. Template - -- Use `docs/PROJECT_STATE_TEMPLATE.md` - ---- - -### Git Commit Strategy - -- **Commit frequency**: Immediately after completing each todo item -- **Don't accumulate**: Don't finish entire todo list then commit -- **Atomic commits**: One commit = one complete, independent change unit - ---- - -### Quality Review - -- **Generated ≠ Complete** -- After completing each todo item: - - Proactively ask if review needed -- Review content: - - Code: logic correctness, type safety, edge cases - - Docs: accuracy, clarity, usable examples -- **Fix issues immediately, don't accumulate** - ---- +### Deployment -### Conversation Review (Auto-execute) +- Deploy `web-client` as the app root +- The same route handlers run in-process and generate config or start/stop agents directly -#### Regular Turns (Simplified) +## Working Rules -``` -Progress: 📐Design → ⚡Execute | Turn 5 | +32 -10 lines -``` - -#### Phase Transition (Full) - -``` -Entering ⚡Execute phase - -[🔍 Clarify] → [📐 Design] → [⚡ Execute] → [✅ Verify] → [📝 Summary] - ▲ Current -``` - -#### Task Complete (Efficiency Stats) - -``` -📊 Session Statistics -Turns: 12 | Tokens: ~8.2k | Changes: +156 -23 ~45 -🤖 15min | 🧑‍💻 3h | ⬇️ 2.75h -``` - -Review standards: `docs/REVIEW_TEMPLATES.md` - ---- - -### Context Management (Mandatory Wrap-up) - -When any of the following occurs: - -- Conversation exceeds 10 turns -- Large code or doc changes -- User indicates insufficient context -- Agent perceives context risk - -**Must execute the following**: - -1. Pause current task -2. Update PROJECT_STATE.md (progress / todos / key decisions) -3. Commit all uncommitted changes -4. Output switch prompt: - -``` -⚠️ Recommend switching to new conversation - -Completed: -- [completed task list] - -To continue: -- [incomplete task list] - -Next: Start new conversation, type "continue " -``` - ---- +- Keep the route handlers usable in both local proxy mode and deployed in-app mode. +- Do not add a global rewrite-based proxy. +- Keep RTC client creation StrictMode-safe. +- Keep transcript speaker mapping based on actual UIDs, not heuristics. +- When adding new env requirements for deployed mode, update `.env.local.example` and the root README. -## Documentation Navigation +## Commands -| Document | Description | -|----------|-------------| -| PROJECT_STATE.md | Project state record (workflow required) | -| ARCHITECTURE.md | Technical architecture and data flow | -| CLAUDE.md | Claude-specific quick reference | -| .claude/skill-overview.md | Skills overview and connection flow | -| .claude/skill-rtc-integration.md | RTC integration with agora-rtc-react hooks | -| .claude/skill-rtm-integration.md | RTM integration guide | -| .claude/skill-conversational-ai-api.md | AgoraVoiceAI (agora-agent-client-toolkit) integration | +From the repo root: ---- - -## Project Overview - -Agora Conversational quick-start Web Demo - A starter template for building real-time conversational applications with Agora SDK. - -**Architecture**: Frontend (React + TypeScript) + Backend (Python FastAPI) - -For detailed architecture, see [ARCHITECTURE.md](./ARCHITECTURE.md) - -## Core Rules - -### Tech Stack - -**Frontend**: -| Item | Value | -|------|-------| -| Package Manager | bun | -| Framework | React | -| Language | TypeScript | -| Styling | Tailwind CSS | -| State Management | Zustand | -| Lint/Format | Biome | - -**Backend**: -| Item | Value | -|------|-------| -| Framework | FastAPI | -| Language | Python 3.6+ | -| Token Generation | Custom TokenBuilder | -| HTTP Client | requests | - -### Key Constraints - -1. **Frontend**: No environment variables needed - all config from backend -2. **Backend**: Must be running on port 8000 before starting frontend -3. Run `bun run lint` before commit and ensure pass -4. Type-first: All components and functions must be properly typed - -### File Naming - -- Files: `kebab-case` -- Components: `PascalCase` - -### Code Style - -Biome enforces consistent formatting and linting rules. - -Path alias: `@/*` → `./src/*` - ---- - -## Common Commands - -**Frontend**: ```bash -bun dev # Start dev server (requires backend running) -bun build # Production build -bun lint # Run Biome lint +bun run frontend +bun run verify:web ``` -**Backend** (from project root): -```bash -cd ../server-python -python3 src/server.py # Start Python service on port 8000 -``` +Useful narrower check: -**Full Stack** (from project root): ```bash -bun run dev # Starts both frontend and backend +bun run verify:web:api +bun run verify:local:fastapi ``` -## Git Commit Format - -```bash -git commit -m "(): +`bun run verify:local:fastapi` boots the FastAPI app and checks the Next proxy path against its real routes, but swaps in a fake agent implementation so the smoke test stays fast and deterministic. -## Changes -- -- +From `web-client/` directly: -Generated with AI Agent" +```bash +bun run doctor +bun run verify ``` - -**Type**: `feat` / `fix` / `docs` / `refactor` / `perf` / `test` / `chore` / `style` diff --git a/web-client/ARCHITECTURE.md b/web-client/ARCHITECTURE.md index a573c11..146009f 100644 --- a/web-client/ARCHITECTURE.md +++ b/web-client/ARCHITECTURE.md @@ -1,10 +1,6 @@ # Agora Conversational AI Web Demo - Architecture -## Overview - -A web demo showcasing quick integration of Agora Conversational AI, featuring real-time voice conversation, subtitle rendering, and log monitoring. - -> **Note**: For AI workflow rules and project specifications, see [AGENTS.md](./AGENTS.md) +This module owns the browser experience and the web-facing `/api/*` entrypoints. ## Tech Stack @@ -16,19 +12,16 @@ A web demo showcasing quick integration of Agora Conversational AI, featuring re | Language | TypeScript | | Build Tool | Turbopack | | Styling | Tailwind CSS | -| State Management | Zustand | | RTC SDK | agora-rtc-react | | RTM SDK | agora-rtm | -| ConvoAI Toolkit | agora-agent-client-toolkit + agora-agent-client-toolkit-react | +| ConvoAI Toolkit | agora-agent-client-toolkit + agora-agent-uikit | -### Backend +### API Ownership -| Category | Technology | -|----------|------------| -| Framework | FastAPI | -| Language | Python 3 | -| Agent SDK | agora-agent-server-sdk | -| Token | agora_agent.agentkit.token (generate_convo_ai_token) | +| Environment | Owner of `/api/*` | Implementation | +|-------------|-------------------|----------------| +| Local dev | Next route handlers with proxy fallback | `app/api/**/route.ts` forwarding to `AGENT_BACKEND_URL` | +| Deployment | Next route handlers in-process | `app/api/**/route.ts` using `src/lib/server/agora.ts` | ## Project Structure @@ -39,20 +32,17 @@ A web demo showcasing quick integration of Agora Conversational AI, featuring re │ └── page.tsx # Home page (loads AgoraProvider + App) ├── src/ # Frontend source │ ├── components/ # UI components -│ │ ├── app.tsx # Main application entry -│ │ ├── subtitle-panel.tsx # Subtitle rendering module -│ │ ├── log-panel.tsx # Log display module -│ │ └── control-bar.tsx # Control buttons (start/stop/mic) +│ │ └── app.tsx # Landing screen + live conversation UI │ ├── hooks/ # React hooks │ │ └── useAgoraConnection.ts # RTC/RTM/VoiceAI connection hook -│ ├── stores/ # State management -│ │ └── app-store.ts # Zustand store │ ├── services/ # Service layer │ │ └── api.ts # Backend API calls (get_config, startAgent, stopAgent) │ └── lib/ # Utility libraries +│ ├── conversation.ts # Transcript + visualizer helpers +│ ├── server/agora.ts # Shared server-side Agora helpers for route handlers │ └── utils.ts # Common utility functions │ -├── proxy.ts # Next.js 16 API proxy (replaces middleware) +├── app/api/ # Route handlers for quick Vercel deployment ├── ../server-python/ # Backend service (project root level) │ ├── src/ │ │ ├── server.py # FastAPI entry, route definitions @@ -66,34 +56,20 @@ A web demo showcasing quick integration of Agora Conversational AI, featuring re ## Core Modules -### 1. SubtitlePanel (Subtitle Rendering) - -- Real-time display of user and AI Agent conversation -- Distinct styling for user/agent messages -- Auto-scroll to latest message -- Message status display via `TurnStatus` from `agora-agent-client-toolkit` +### 1. App (Landing + Conversation) -### 2. LogPanel (Log Display) - -- System runtime logs -- Log level support (info/success/error/warning) -- Timestamp display -- Collapsible/expandable - -### 3. ControlBar (Control Bar) - -- Start/Stop Agent button -- Microphone toggle button -- Agent state indicator via `AgentState` from `agora-agent-client-toolkit` +- Landing-page-to-conversation transition aligned with the Next.js quickstart +- Uses `AgentVisualizer`, `ConvoTextStream`, and `MicButtonWithVisualizer` from `agora-agent-uikit` +- Keeps the end-call and mic controls in the same docked conversation layout as the reference sample ## Data Flow ``` User Action → useAgoraConnection hook → Agora SDK (agora-rtc-react) ↓ - Zustand Store ← AgoraVoiceAI Events (agora-agent-client-toolkit) + AgoraVoiceAI Events (agora-agent-client-toolkit) ↓ - UI Components + UIKit transcript + visualizer components ``` ## API Endpoints @@ -127,8 +103,8 @@ User Action → useAgoraConnection hook → Agora SDK (agora-rtc-react) // Request { "channelName": "test-channel", - "rtcUid": "12345678", - "userUid": "123456" + "rtcUid": 12345678, + "userUid": 123456 } // Response @@ -157,76 +133,63 @@ User Action → useAgoraConnection hook → Agora SDK (agora-rtc-react) } ``` -## Backend Architecture +## Environment Modes -### Python Service (FastAPI) +### Local Python-Backed Development ``` +app/api/ +├── get_config/route.ts +└── v2/ + ├── startAgent/route.ts + └── stopAgent/route.ts + +Optional local backend: ../server-python/ ├── src/ -│ ├── server.py # FastAPI app, routes, CORS -│ └── agent.py # Agent start/stop logic -└── requirements.txt # Python dependencies +│ ├── server.py +│ └── agent.py +└── requirements.txt ``` -### Environment Variables +In this mode, the web client still receives all browser requests. The route handlers proxy to Python through `AGENT_BACKEND_URL`. -Backend reads configuration from `server-python/.env.local`: +### Single-Target Web Deployment -| Variable | Description | -|----------|-------------| -| APP_ID | Agora App ID | -| APP_CERTIFICATE | Agora App Certificate | -| ASR_DEEPGRAM_API_KEY | Deepgram ASR API Key | -| LLM_API_KEY | LLM API Key | -| TTS_ELEVENLABS_API_KEY | ElevenLabs TTS API Key | -| PORT | Backend server port (default: 8000) | +The same `app/api/**/route.ts` files run directly inside the deployed Next app. No separate Python service is required. -### Proxy Configuration +## Environment Variables -Next.js 16 uses `proxy.ts` to proxy `/api/*` requests to Python backend (port 8000): +The web client route handlers can read configuration from `web-client/.env.local` or Vercel project env vars: -```typescript -// proxy.ts -export function GET(request: NextRequest) { - const url = new URL(request.url) - const backendUrl = `http://localhost:8000${url.pathname.replace('/api', '')}` - return fetch(backendUrl, { headers: request.headers }) -} -``` +| Variable | Description | +|----------|-------------| +| AGORA_APP_ID | Agora App ID | +| AGORA_APP_CERTIFICATE | Agora App Certificate | +| AGENT_GREETING | Optional custom greeting for the Ada persona | +| AGENT_BACKEND_URL | Optional Python backend URL for local proxy mode | -## State Management (Zustand) +## Local Proxy Mode + +When `AGENT_BACKEND_URL` is set, the Next route handlers forward `/api/*` requests to the Python backend: ```typescript -interface AppState { - // Connection - isConnected: boolean - isConnecting: boolean - channelName: string - - // Agent - agentId: string | null - agentState: AgentState // from agora-agent-client-toolkit - - // Audio - isMicMuted: boolean - - // Transcripts - transcripts: TranscriptItem[] // uses TurnStatus from agora-agent-client-toolkit - - // Logs - logs: LogItem[] -} +const proxiedResponse = await proxyToPythonBackend('v2/startAgent', { + method: 'POST', + body: JSON.stringify({ channelName, rtcUid, userUid }), +}) ``` ## Event Flow 1. User clicks connect → Call `/api/get_config` to get configuration -2. AgoraRTCProvider creates RTC client -3. useAgoraConnection hook manages connection via `useJoin`, `usePublish` hooks -4. Use returned token to login RTM → Join RTC channel -5. Initialize `AgoraVoiceAI` from `agora-agent-client-toolkit` (imperative API) -6. `voiceAI.subscribeMessage(channel)` to listen for subtitle/state events -7. Call `/api/v2/startAgent` to start Agent -8. Listen to `AgoraVoiceAIEvents` → Update Zustand store → UI re-renders -9. User clicks stop → Call `/api/v2/stopAgent` → Cleanup resources +2. `AgoraRTCProvider` creates exactly one RTC client via `useRef`, which avoids StrictMode double creation +3. `useAgoraConnection` gates `useJoin` and `useLocalMicrophoneTrack` behind a readiness flag +4. `/api/*` is handled in-process in deployed mode, or proxied to Python when `AGENT_BACKEND_URL` is set +5. Use returned token to login RTM and join RTC +6. Initialize `AgoraVoiceAI` from `agora-agent-client-toolkit` +7. `voiceAI.subscribeMessage(channel)` listens for transcript and agent-state events +8. Call `/api/v2/startAgent` to start the requester-scoped agent session +9. Normalize local transcript UID `0` back to the actual RTC UID before rendering `ConvoTextStream` +10. Renew RTC and RTM tokens through `/api/get_config?channel=...&uid=...` before expiry +11. User clicks stop → Call `/api/v2/stopAgent` → Cleanup resources diff --git a/web-client/CLAUDE.md b/web-client/CLAUDE.md index 28adc8c..71dd3d2 100644 --- a/web-client/CLAUDE.md +++ b/web-client/CLAUDE.md @@ -1,46 +1,10 @@ # Claude AI Assistant Guidelines -> **Primary Reference**: See **[AGENTS.md](./AGENTS.md)** for complete AI workflow rules and project specifications. +Use [AGENTS.md](./AGENTS.md) as the source of truth for this module. -This file contains Claude-specific quick references and skill document pointers. +The only Claude-specific note here is that `web-client` supports two valid runtime shapes: -## Skill Documents (.claude/) +- local development, where Next `/api/*` handlers proxy to FastAPI through `AGENT_BACKEND_URL` +- deployment, where those same route handlers run in-process -When working on Agora SDK integration, reference these skill documents: - -| Skill | File | Keywords | -|-------|------|----------| -| Overview | `.claude/skill-overview.md` | Integration workflow, connection flow | -| RTC Integration | `.claude/skill-rtc-integration.md` | `RTC`, `audio`, `microphone`, `AgoraRTC` | -| RTM Integration | `.claude/skill-rtm-integration.md` | `RTM`, `message`, `subscribe`, `AgoraRTM` | -| ConversationalAI API | `.claude/skill-conversational-ai-api.md` | `subtitle`, `transcript`, `agent state` | - -**Auto-activation**: When user mentions keywords, automatically reference the corresponding skill document. - -## Quick Commands - -See [AGENTS.md - Common Commands](./AGENTS.md#common-commands) for full list. - -```bash -bun dev # Start both frontend and backend -bun run lint # Run Biome lint (required before commit) -``` - -## Critical Constraints - -See [AGENTS.md - Key Constraints](./AGENTS.md#key-constraints) for details: - -1. Frontend gets all config from backend (no env vars) -2. Backend must run on port 8000 before frontend starts -3. Run `bun run lint` before commit -4. Type-first: All code must be properly typed - -## Documentation Map - -| Document | Purpose | Audience | -|----------|---------|----------| -| [AGENTS.md](./AGENTS.md) | AI workflow + project specs | AI Agents | -| [ARCHITECTURE.md](./ARCHITECTURE.md) | Technical architecture | Developers | -| [README.md](./README.md) | User guide | End users | -| `../server-python/AGENTS.md` | Backend-specific rules | AI Agents | -| `../server-python/README.md` | Backend setup | Developers | +Before describing the request flow or verification steps, check `AGENTS.md` and the repo-root [README.md](../README.md). diff --git a/web-client/app/api/get_config/route.ts b/web-client/app/api/get_config/route.ts new file mode 100644 index 0000000..7f11789 --- /dev/null +++ b/web-client/app/api/get_config/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server' +import { + generateChannelName, + generateRtcAndRtmToken, + getAgoraCredentials, + proxyToPythonBackend, +} from '@/lib/server/agora' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + try { + const proxiedResponse = await proxyToPythonBackend('get_config', undefined, request.nextUrl.searchParams) + if (proxiedResponse) { + const body = await proxiedResponse.text() + return new NextResponse(body, { + status: proxiedResponse.status, + headers: { 'Content-Type': proxiedResponse.headers.get('Content-Type') ?? 'application/json' }, + }) + } + + const { appId } = getAgoraCredentials() + const uid = request.nextUrl.searchParams.get('uid') ?? `${Math.floor(Math.random() * 9_999_000) + 1000}` + const parsedUid = Number.parseInt(uid, 10) + const rtcUid = Number.isNaN(parsedUid) ? 0 : parsedUid + const channelName = request.nextUrl.searchParams.get('channel') ?? generateChannelName() + const agentUid = `${Math.floor(Math.random() * 89_999_999) + 10_000_000}` + const token = generateRtcAndRtmToken(channelName, rtcUid) + + return NextResponse.json({ + code: 0, + data: { + app_id: appId, + token, + uid: uid.toString(), + channel_name: channelName, + agent_uid: agentUid, + }, + msg: 'success', + }) + } catch (error) { + console.error('Error generating config:', error) + return NextResponse.json( + { + detail: error instanceof Error ? error.message : 'Failed to generate config', + }, + { status: 500 }, + ) + } +} diff --git a/web-client/app/api/v2/startAgent/route.ts b/web-client/app/api/v2/startAgent/route.ts new file mode 100644 index 0000000..3c6026b --- /dev/null +++ b/web-client/app/api/v2/startAgent/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server' +import { + createAgoraClient, + createManagedAgent, + ExpiresIn, + proxyToPythonBackend, +} from '@/lib/server/agora' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { channelName, rtcUid, userUid } = body as { + channelName?: string + rtcUid?: number + userUid?: number + } + + if (!channelName || !Number.isInteger(rtcUid) || !Number.isInteger(userUid)) { + return NextResponse.json({ detail: 'channelName, rtcUid, and userUid are required' }, { status: 400 }) + } + + const proxiedResponse = await proxyToPythonBackend('v2/startAgent', { + method: 'POST', + body: JSON.stringify({ channelName, rtcUid, userUid }), + }) + if (proxiedResponse) { + const responseBody = await proxiedResponse.text() + return new NextResponse(responseBody, { + status: proxiedResponse.status, + headers: { 'Content-Type': proxiedResponse.headers.get('Content-Type') ?? 'application/json' }, + }) + } + + const client = createAgoraClient() + const agent = createManagedAgent() + const session = agent.createSession(client, { + channel: channelName, + agentUid: String(rtcUid), + remoteUids: [String(userUid)], + idleTimeout: 30, + expiresIn: ExpiresIn.hours(1), + debug: false, + }) + + const agentId = await session.start() + + return NextResponse.json({ + code: 0, + data: { + agent_id: agentId, + channel_name: channelName, + status: 'started', + }, + msg: 'success', + }) + } catch (error) { + console.error('Error starting agent:', error) + return NextResponse.json( + { + detail: error instanceof Error ? error.message : 'Failed to start agent', + }, + { status: 500 }, + ) + } +} diff --git a/web-client/app/api/v2/stopAgent/route.ts b/web-client/app/api/v2/stopAgent/route.ts new file mode 100644 index 0000000..f5db826 --- /dev/null +++ b/web-client/app/api/v2/stopAgent/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server' +import { + createAgoraClient, + isAgentAlreadyStoppingOrStopped, + proxyToPythonBackend, +} from '@/lib/server/agora' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { agentId } = body as { agentId?: string } + + if (!agentId) { + return NextResponse.json({ detail: 'agentId is required' }, { status: 400 }) + } + + const proxiedResponse = await proxyToPythonBackend('v2/stopAgent', { + method: 'POST', + body: JSON.stringify({ agentId }), + }) + if (proxiedResponse) { + const responseBody = await proxiedResponse.text() + return new NextResponse(responseBody, { + status: proxiedResponse.status, + headers: { 'Content-Type': proxiedResponse.headers.get('Content-Type') ?? 'application/json' }, + }) + } + + const client = createAgoraClient() + try { + await client.stopAgent(agentId) + } catch (error) { + if (!isAgentAlreadyStoppingOrStopped(error)) { + throw error + } + } + + return NextResponse.json({ code: 0, msg: 'success' }) + } catch (error) { + console.error('Error stopping agent:', error) + return NextResponse.json( + { + detail: error instanceof Error ? error.message : 'Failed to stop agent', + }, + { status: 500 }, + ) + } +} diff --git a/web-client/app/page.tsx b/web-client/app/page.tsx index 70652d3..b02d323 100644 --- a/web-client/app/page.tsx +++ b/web-client/app/page.tsx @@ -1,7 +1,7 @@ 'use client' import dynamic from 'next/dynamic' -import { useMemo } from 'react' +import { useRef } from 'react' const AgoraProvider = dynamic( async () => { @@ -14,8 +14,11 @@ const AgoraProvider = dynamic( return { default: ({ children }: { children: React.ReactNode }) => { - const client = useMemo(() => AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' }), []) - return {children} + const clientRef = useRef | null>(null) + if (!clientRef.current) { + clientRef.current = AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' }) + } + return {children} }, } }, diff --git a/web-client/bun.lock b/web-client/bun.lock deleted file mode 100644 index 7a6036b..0000000 --- a/web-client/bun.lock +++ /dev/null @@ -1,422 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "agora-conversational-ai-demo", - "dependencies": { - "agora-agent-client-toolkit": "^0.1.1", - "agora-agent-client-toolkit-react": "^0.1.1", - "agora-rtc-react": "^2.2.0", - "agora-rtc-sdk-ng": "^4.24.2", - "agora-rtm": "^2.2.3", - "next": "^16.1.6", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "zustand": "^4.5.0", - }, - "devDependencies": { - "@biomejs/biome": "^1.9.0", - "@types/node": "^25.3.1", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "autoprefixer": "^10.4.18", - "postcss": "^8.4.35", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.0", - }, - }, - }, - "packages": { - "@agora-js/media": ["@agora-js/media@4.24.2", "", { "dependencies": { "@agora-js/report": "4.24.2", "@agora-js/shared": "4.24.2", "agora-rte-extension": "^1.2.4", "axios": "^1.13.2", "webrtc-adapter": "8.2.0" } }, "sha512-NVi4bg9Ytn4SCeFj90HobpMqzecoH4lAkNkGq+GsfmtmJBxwagufaOPU/4RYH70sRnV4cY3HXO59UHef2tj4iA=="], - - "@agora-js/report": ["@agora-js/report@4.24.2", "", { "dependencies": { "@agora-js/shared": "4.24.2", "axios": "^1.13.2" } }, "sha512-cAYi67dZhFTU+cQGxbMrD2TEJXJPBnXJSiWtXBtQ+Oxw+9Qi/dfwX/PwIvqwqmC3jjJJjwZpTZOJCuBpAHOOfQ=="], - - "@agora-js/shared": ["@agora-js/shared@4.24.2", "", { "dependencies": { "axios": "^1.13.2", "ua-parser-js": "^0.7.34" } }, "sha512-BACC0Z46rWVHdgMIfDX+5YnLYwAIMo+971XxD4mh1pVD5LiwlbM/rQGBKjVpPJoMvFHlGW8L1/eMmRFRppjChw=="], - - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - - "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], - - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="], - - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="], - - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="], - - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="], - - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="], - - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="], - - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="], - - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - - "@types/node": ["@types/node@25.3.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw=="], - - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - - "agora-agent-client-toolkit": ["agora-agent-client-toolkit@0.1.1", "", { "optionalDependencies": { "@agora-js/report": ">=4.19.0", "jszip": ">=3.0.0" }, "peerDependencies": { "agora-rtc-sdk-ng": ">=4.23.4", "agora-rtm": ">=2.0.0" }, "optionalPeers": ["agora-rtm"] }, "sha512-eV/BRpKq4rRhDDlvM8z4COrhFe4AgMuyXcZPxHM9ZMYivUYeSmTvDj10JdI/lH4/NEa0lc1nH8H/ZWCeRplHZw=="], - - "agora-agent-client-toolkit-react": ["agora-agent-client-toolkit-react@0.1.1", "", { "peerDependencies": { "agora-agent-client-toolkit": ">=0.1.0", "agora-rtc-react": ">=2.0.0", "react": ">=18.0.0" } }, "sha512-sFaEyDtZp8XW8h/MeURiQfXjgLAR/wuRY9qLopX8D6m5U5lfID9KXr3E8J04sX6HuMCcUO8OgPhjL7sCInUGYA=="], - - "agora-rtc-react": ["agora-rtc-react@2.5.1", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-ORPbV02p8dZ2GhQxo5kTarc0BYjObc2IeqRPv9mi5/fxDI5nMY6kg54R5fUvLQU05H3Q5Kecq7M2kuZzHfNo5Q=="], - - "agora-rtc-sdk-ng": ["agora-rtc-sdk-ng@4.24.2", "", { "dependencies": { "@agora-js/media": "4.24.2", "@agora-js/report": "4.24.2", "@agora-js/shared": "4.24.2", "agora-rte-extension": "^1.2.4", "axios": "^1.13.2", "formdata-polyfill": "^4.0.7", "pako": "^2.1.0", "ua-parser-js": "^0.7.34", "webrtc-adapter": "8.2.0" } }, "sha512-Oy4F8c+B6esQt8dYY8QYHKUUbWNf4SeM025tG8giWEsnytX7NB8EeINfYGMyFXoeVMqNyJO7NdAeK6lBY5c5oQ=="], - - "agora-rte-extension": ["agora-rte-extension@1.2.4", "", {}, "sha512-0ovZz1lbe30QraG1cU+ji7EnQ8aUu+Hf3F+a8xPml3wPOyUQEK6CTdxV9kMecr9t+fIDrGeW7wgJTsM1DQE7Nw=="], - - "agora-rtm": ["agora-rtm@2.2.3", "", { "peerDependencies": { "agora-rtc-sdk-ng": "4.23.0" } }, "sha512-5O6uINrtnNDXijZEAmTwLzC1J6GizQoRb4moRx+DsVeYjcEbMjZAFcSnZVYHsjq3LrWetdknMhGt2o0zZJEBtQ=="], - - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - - "autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="], - - "axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], - - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="], - - "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - - "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], - - "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], - - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - - "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - - "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], - - "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], - - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "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" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], - - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - - "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], - - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], - - "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], - - "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - - "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], - - "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], - - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - - "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], - - "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - - "sdp": ["sdp@3.2.1", "", {}, "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw=="], - - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], - - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "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" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], - - "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], - - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="], - - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - - "webrtc-adapter": ["webrtc-adapter@8.2.0", "", { "dependencies": { "sdp": "^3.0.2" } }, "sha512-umxCMgedPAVq4Pe/jl3xmelLXLn4XZWFEMR5Iipb5wJ+k1xMX0yC4ZY9CueZUU1MjapFxai1tFGE7R/kotH6Ww=="], - - "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], - - "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "jszip/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - - "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - } -} diff --git a/web-client/docs/ARCHITECTURE.md b/web-client/docs/ARCHITECTURE.md deleted file mode 100644 index 9af30e2..0000000 --- a/web-client/docs/ARCHITECTURE.md +++ /dev/null @@ -1,91 +0,0 @@ -# Architecture - -## 1. Tech Stack & Project Scope - -- Project type: Single repo -- Frontend framework: React -- Language: TypeScript -- Build tool: Vite (recommended for React + bun) -- UI/Styling: Tailwind CSS -- State management: Zustand -- Data fetching & caching: React Query -- SSR/Isomorphic: None - -## 2. Package Management & Toolchain - -- Runtime: Bun (latest stable) -- Package manager: bun -- Lint/Format: Biome -- Testing: Vitest -- CI/CD: GitHub Actions -- Code generation: None - -## 3. Runtime & Environment - -- Environment variables: `.env` files -- Required variables: - - `VITE_AGORA_APP_ID` - Agora App ID - - `VITE_AGORA_TOKEN` - Agora Token (optional for testing) -- Local/Test/Prod differences: Environment-specific `.env` files -- Entry point: `src/main.tsx` -- Default port: 5173 (Vite default) -- Secrets handling: Never commit `.env` files, use `.env.example` as template - -## 4. Dependencies & External Services - -- Backend services: - - Agora RTC SDK - Real-time communication - - Agora Conversational AI - Voice/text conversation -- Private dependencies: None -- Third-party services: - - Agora (authentication via App ID/Token) - -## 5. Module Responsibilities & Directory Structure - -``` -src/ -├── components/ # Reusable UI components -├── hooks/ # Custom React hooks -├── stores/ # Zustand stores -├── services/ # API and Agora service wrappers -├── types/ # TypeScript type definitions -├── utils/ # Utility functions -├── pages/ # Page components (if using routing) -├── App.tsx # Root component -└── main.tsx # Entry point -``` - -## 6. Data Flow & Routing - -- Routing: React Router (if needed) or single-page -- Data flow: - 1. User action triggers hook/component - 2. React Query manages server state - 3. Zustand manages client state - 4. Components re-render on state change -- Error handling: React Query error boundaries + toast notifications -- Loading states: React Query loading states + skeleton components - -## 7. Build & Deployment - -- Build command: `bun run build` -- Output: `dist/` -- Deployment target: Static hosting (Vercel, Netlify, etc.) -- Version strategy: Semantic versioning -- Rollback: Redeploy previous version - -## 8. Constraints & Known Issues - -- Known constraints: - - Agora SDK requires HTTPS in production - - WebRTC requires user permission for microphone/camera -- Known pitfalls: - - Token expiration handling - - Network reconnection logic -- Behaviors that must not break: - - Real-time audio/video streaming - - Conversation state persistence - -## 9. Update Log - -- 2026-01-21: Initial architecture document created diff --git a/web-client/docs/PROJECT_STATE_TEMPLATE.md b/web-client/docs/PROJECT_STATE_TEMPLATE.md deleted file mode 100644 index 77e4b89..0000000 --- a/web-client/docs/PROJECT_STATE_TEMPLATE.md +++ /dev/null @@ -1,35 +0,0 @@ -# PROJECT_STATE.md (Project State Record) - -Purpose: Record goals, context, notes, todos, and review results. Can be used directly as PR description basis. - -## Goal - -- - -## Context Summary - -- - -## Notes - -- - -## Todo (with checkboxes) - -- [ ] -- [ ] - -## Completed/Change Summary (PR-ready) - -- Change summary: -- Impact scope: -- Verification method: -- Risk and rollback: - -## Session Review Log (append per session) - -### Session - -- Conversation review: -- Change review: -- Review notes: diff --git a/web-client/docs/REVIEW_TEMPLATES.md b/web-client/docs/REVIEW_TEMPLATES.md deleted file mode 100644 index 3df5fa3..0000000 --- a/web-client/docs/REVIEW_TEMPLATES.md +++ /dev/null @@ -1,130 +0,0 @@ -# Review Standards (Agent Self-check) - -Agent auto-reviews at end of each phase using these standards, outputs review conclusion. User doesn't need to participate unless explicitly requesting interactive review. - ---- - -## Usage - -- **Default mode**: Agent auto-reviews, outputs progress and efficiency metrics -- **Interactive mode**: When user says "detailed review" or "check each item", show item by item - ---- - -## Phase Indicators - -### Full Progress Bar (shown on phase transition) - -``` -[🔍 Clarify] → [📐 Design] → [⚡ Execute] → [✅ Verify] → [📝 Summary] - ▲ Current -``` - -### Phase Descriptions - -| Icon | Phase | Meaning | Allowed Actions | -|------|-------|---------|-----------------| -| 🔍 | Clarify | Understand requirements | Ask, decompose, no assumptions | -| 📐 | Design | Plan solution | Structured reasoning, give options | -| ⚡ | Execute | Code/write docs | Direct action, no restating | -| ✅ | Verify | Check results | Compare against criteria, flag risks | -| 📝 | Summary | Consolidate output | Structured summary | - -### Phase Transition - -Clearly notify user when switching phases, e.g.: - -``` -Entering ⚡Execute phase - -[🔍 Clarify] → [📐 Design] → [⚡ Execute] → [✅ Verify] → [📝 Summary] - ▲ Current -``` - ---- - -## Conversation Review (Agent Self-check) - -Agent self-checks before each output: - -1. **Correct phase** - Which phase am I in? Crossing phases? -2. **Converged output** - Giving finite options vs unbounded divergence? -3. **Avoid redundancy** - Repeating user-confirmed content? -4. **Blocker handling** - Missing info → interrupt vs assume? - -Self-check pass → Output directly -Self-check fail → Adjust then output, or explain blocker - ---- - -## Change Review (Pre-commit Self-check) - -Agent self-checks before committing code/PR: - -Required Items -- [ ] Only made changes within declared scope -- [ ] No unexpected behavior changes -- [ ] Ran lint -- [ ] Explained verification method - -Extended Items (if applicable) -- [ ] Public component/config changes explained impact -- [ ] New dependencies explained necessity -- [ ] Rollback suggestion (if needed) - ---- - -## Output Format - -### Simplified (end of each output) - -``` -Progress: 📐Design → ⚡Execute | Turn 5 | +32 -10 lines -``` - -### Efficiency Metrics (phase end or task complete) - -``` -📊 Session Statistics -Turns: 12 | Tokens: ~8.2k | Changes: +156 -23 ~45 -🤖 15min | 🧑‍💻 3h | ⬇️ 2.75h -``` - -- 🤖 = AI actual time -- 🧑‍💻 = Human estimated time -- ⬇️ = Time saved - -### Needs Confirmation - -``` -⚠️ Needs confirmation | 📐Design | Changes exceed declared scope, continue? -``` - -### Blocked - -``` -❌ Blocked | 🔍Clarify | Missing required info, please provide XXX -``` - ---- - -## Interactive Review (On-demand Only) - -Enter interactive mode when user says: -- "detailed review" -- "check each item" -- "expand review" - -In interactive mode, show check items and results one by one. - ---- - -## Emoji Usage Rules - -| Context | Rule | -|---------|------| -| Conversation | ✅ Use freely, replace text | -| PROJECT_STATE.md | ✅ Use freely | -| Docs (AGENTS.md etc.) | ✅ Use freely | -| Code comments | ⚠️ Use sparingly | -| Commit messages | ❌ Never use | diff --git a/web-client/docs/WORKFLOW_TEMPLATES.md b/web-client/docs/WORKFLOW_TEMPLATES.md deleted file mode 100644 index 84628ad..0000000 --- a/web-client/docs/WORKFLOW_TEMPLATES.md +++ /dev/null @@ -1,123 +0,0 @@ -# Workflow Templates - -Before starting a task, select the corresponding type and follow the steps. If unsure, default to `fix` with additional notes. - -Universal constraint: Must update `PROJECT_STATE.md` at end of each phase. - ---- - -## feat (New Feature/Page) - -Entry Routing -1. Adding new page/route? -2. Adding new config or data source? -3. Involves new copy or i18n? - -Minimum Questions -- Target page/route name? -- Expected behavior and acceptance criteria? - -Action Checklist -- [ ] Confirm route and directory conventions -- [ ] Reuse or create components -- [ ] Add config and type definitions -- [ ] Update copy (if i18n) -- [ ] Run lint - -Completion Criteria -- Page/feature works, meets acceptance criteria -- Lint passes - ---- - -## fix (Bug Fix) - -Entry Routing -1. Reproducible? -2. Error logs available? -3. Caused by recent changes? - -Minimum Questions -- Repro steps + expected behavior + actual behavior? -- Affected page/component/API? - -Action Checklist -- [ ] Locate trigger path and responsibility boundary -- [ ] Minimal fix -- [ ] Regression check - -Completion Criteria -- Bug no longer reproduces -- Lint passes - ---- - -## refactor (Refactoring) - -Entry Routing -1. Changes external behavior? -2. Needs phased approach? - -Minimum Questions -- Refactor motivation and scope? -- Invariants and behaviors that must not break? - -Action Checklist -- [ ] Define boundaries and invariants -- [ ] Small-step replacement with verification -- [ ] Update related docs - -Completion Criteria -- External behavior unchanged -- Lint/Test passes - ---- - -## chore (Engineering/Dependencies/Config) - -Entry Routing -1. Involves dependency upgrade? -2. Affects build/CI? - -Minimum Questions -- Target dependency/config? -- Change motivation and risks? - -Action Checklist -- [ ] Update dependency or config -- [ ] Run lint/build/test -- [ ] Document compatibility changes - -Completion Criteria -- Change takes effect -- Key scripts pass - ---- - -## docs (Documentation Update) - -Entry Routing -1. New documentation? -2. Involves spec changes? - -Action Checklist -- [ ] Update docs and links -- [ ] Mark update timestamp - -Completion Criteria -- Docs readable and consistent - ---- - -## test (Testing) - -Entry Routing -1. Adding new tests? -2. Fixing test failures? - -Action Checklist -- [ ] Add/fix tests -- [ ] Ensure stability - -Completion Criteria -- Test cases pass stably diff --git a/web-client/next.config.ts b/web-client/next.config.ts index 9c845ce..e96a25d 100644 --- a/web-client/next.config.ts +++ b/web-client/next.config.ts @@ -1,8 +1,12 @@ import type { NextConfig } from 'next' +import path from 'node:path' const nextConfig: NextConfig = { // Enable React strict mode reactStrictMode: true, + turbopack: { + root: path.resolve(__dirname, '..'), + }, // Optimize images images: { diff --git a/web-client/package.json b/web-client/package.json index f9f5fd0..00770e9 100644 --- a/web-client/package.json +++ b/web-client/package.json @@ -2,21 +2,26 @@ "name": "agora-conversational-ai-demo", "version": "1.0.0", "scripts": { + "doctor": "bun run scripts/doctor.ts", "dev": "next dev", "build": "next build", "start": "next start", - "lint": "biome check --write ." + "lint": "biome check .", + "lint:fix": "biome check --write .", + "verify:api": "bun run scripts/verify-api-contracts.ts", + "verify": "bun run doctor && bun run verify:api && bun run build" }, "dependencies": { - "agora-agent-client-toolkit-react": "^0.1.1", - "agora-agent-client-toolkit": "^0.1.1", - "agora-rtc-react": "^2.2.0", - "agora-rtc-sdk-ng": "^4.24.2", + "agora-agent-client-toolkit": "1.2.0", + "agora-agent-server-sdk": "^1.3.2", + "agora-agent-uikit": "1.1.0", + "agora-rtc-react": "^2.5.1", + "agora-rtc-sdk-ng": "^4.24.3", "agora-rtm": "^2.2.3", + "agora-token": "^2.0.5", "next": "^16.1.6", "react": "^19.2.4", - "react-dom": "^19.2.4", - "zustand": "^4.5.0" + "react-dom": "^19.2.4" }, "devDependencies": { "@biomejs/biome": "^1.9.0", @@ -28,4 +33,4 @@ "tailwindcss": "^3.4.1", "typescript": "^5.3.0" } -} \ No newline at end of file +} diff --git a/web-client/proxy.ts b/web-client/proxy.ts deleted file mode 100644 index 2ed0934..0000000 --- a/web-client/proxy.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -export function proxy(request: NextRequest) { - const { pathname } = request.nextUrl - - // Proxy API requests to Python backend - if (pathname.startsWith('/api/')) { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000' - const backendPath = pathname.replace('/api', '') - const backendUrl = new URL(backendPath, apiUrl) - - // Copy search params - request.nextUrl.searchParams.forEach((value, key) => { - backendUrl.searchParams.set(key, value) - }) - - return NextResponse.rewrite(backendUrl) - } - - return NextResponse.next() -} - -export const config = { - matcher: '/api/:path*', -} diff --git a/web-client/scripts/doctor.ts b/web-client/scripts/doctor.ts new file mode 100644 index 0000000..5ace14f --- /dev/null +++ b/web-client/scripts/doctor.ts @@ -0,0 +1,70 @@ +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' + +function fail(message: string): never { + console.error(message) + process.exit(1) +} + +function loadEnvFile(filePath: string): Record { + if (!existsSync(filePath)) { + return {} + } + + const contents = readFileSync(filePath, 'utf8') + const result: Record = {} + for (const rawLine of contents.split('\n')) { + const line = rawLine.trim() + if (!line || line.startsWith('#')) continue + + const separatorIndex = line.indexOf('=') + if (separatorIndex <= 0) continue + + const key = line.slice(0, separatorIndex).trim() + const value = line.slice(separatorIndex + 1).trim() + result[key] = value + } + + return result +} + +const cwd = process.cwd() +const envPath = path.join(cwd, '.env.local') +const examplePath = path.join(cwd, '.env.local.example') + +if (!existsSync(examplePath)) { + fail('Missing .env.local.example. Restore the tracked template before continuing.') +} + +const fileEnv = loadEnvFile(envPath) +const mergedEnv = { + ...fileEnv, + ...Object.fromEntries(Object.entries(process.env).filter(([, value]) => typeof value === 'string')), +} + +const backendUrl = mergedEnv.AGENT_BACKEND_URL +if (backendUrl) { + try { + const parsed = new URL(backendUrl) + if (!['http:', 'https:'].includes(parsed.protocol)) { + throw new Error('unsupported protocol') + } + } catch { + fail('AGENT_BACKEND_URL must be a valid http(s) URL.') + } + + console.log(`Doctor checks passed for local proxy mode (${backendUrl})`) + process.exit(0) +} + +if (!existsSync(envPath) && !mergedEnv.AGORA_APP_ID && !mergedEnv.AGORA_APP_CERTIFICATE) { + fail('Missing .env.local. Copy .env.local.example to .env.local or set AGORA_APP_ID and AGORA_APP_CERTIFICATE in the environment.') +} + +for (const key of ['AGORA_APP_ID', 'AGORA_APP_CERTIFICATE']) { + if (!mergedEnv[key]?.trim()) { + fail(`Missing ${key}. Set it in .env.local or the environment before running the standalone web client.`) + } +} + +console.log('Doctor checks passed for standalone web mode') diff --git a/web-client/scripts/verify-api-contracts.ts b/web-client/scripts/verify-api-contracts.ts new file mode 100644 index 0000000..9438b1d --- /dev/null +++ b/web-client/scripts/verify-api-contracts.ts @@ -0,0 +1,166 @@ +import { NextRequest } from 'next/server' +import { AgoraClient, Agent } from 'agora-agent-server-sdk' +import { RtcTokenBuilder } from 'agora-token' + +import { GET as getConfig } from '../app/api/get_config/route' +import { POST as startAgent } from '../app/api/v2/startAgent/route' +import { POST as stopAgent } from '../app/api/v2/stopAgent/route' + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message) + } +} + +function getJson(response: Response) { + return response.json() as Promise> +} + +async function verifyGetConfigRoute() { + process.env.AGORA_APP_ID = '0123456789abcdef0123456789abcdef' + process.env.AGORA_APP_CERTIFICATE = 'fedcba9876543210fedcba9876543210' + delete process.env.AGENT_BACKEND_URL + + const originalBuildTokenWithRtm = RtcTokenBuilder.buildTokenWithRtm + let tokenBuilderArgs: unknown[] | null = null + + RtcTokenBuilder.buildTokenWithRtm = ((...args: unknown[]) => { + tokenBuilderArgs = args + return 'mock-rtc-rtm-token' + }) as typeof RtcTokenBuilder.buildTokenWithRtm + + try { + const request = new NextRequest('http://localhost:3000/api/get_config?uid=1234&channel=test-channel') + const response = await getConfig(request) + const body = await getJson(response) + + assert(response.status === 200, 'GET /api/get_config should return 200') + assert(body.code === 0, 'GET /api/get_config should return code 0') + + const data = body.data as Record | undefined + assert(data, 'GET /api/get_config should include data') + assert(data?.app_id === process.env.AGORA_APP_ID, 'GET /api/get_config should echo app_id') + assert(data?.uid === '1234', 'GET /api/get_config should preserve uid') + assert(data?.channel_name === 'test-channel', 'GET /api/get_config should preserve channel_name') + assert(data?.token === 'mock-rtc-rtm-token', 'GET /api/get_config should return the RTC+RTM token') + assert(typeof data?.agent_uid === 'string' && data.agent_uid.length > 0, 'GET /api/get_config should return agent_uid') + + assert(Array.isArray(tokenBuilderArgs), 'GET /api/get_config should call buildTokenWithRtm') + assert(tokenBuilderArgs?.[2] === 'test-channel', 'buildTokenWithRtm should use the requested channel') + assert(tokenBuilderArgs?.[3] === 1234, 'buildTokenWithRtm should receive an int uid') + } finally { + RtcTokenBuilder.buildTokenWithRtm = originalBuildTokenWithRtm + } +} + +async function verifyStartAgentValidation() { + delete process.env.AGENT_BACKEND_URL + + const request = new NextRequest('http://localhost:3000/api/v2/startAgent', { + body: JSON.stringify({ channelName: 'missing-uids' }), + method: 'POST', + }) + const response = await startAgent(request) + const body = await getJson(response) + + assert(response.status === 400, 'POST /api/v2/startAgent should reject missing fields') + assert(body.detail === 'channelName, rtcUid, and userUid are required', 'POST /api/v2/startAgent should explain validation failure') +} + +async function verifyStartAgentSuccess() { + process.env.AGORA_APP_ID = '0123456789abcdef0123456789abcdef' + process.env.AGORA_APP_CERTIFICATE = 'fedcba9876543210fedcba9876543210' + delete process.env.AGENT_BACKEND_URL + + const originalCreateSession = Agent.prototype.createSession + let capturedSessionConfig: { channel?: string; agentUid?: string; remoteUids?: string[] } | null = null + + Agent.prototype.createSession = ((_: unknown, sessionConfig: unknown) => { + capturedSessionConfig = sessionConfig as { channel?: string; agentUid?: string; remoteUids?: string[] } + return { + start: async () => 'mock-agent-id', + } + }) as unknown as typeof Agent.prototype.createSession + + try { + const request = new NextRequest('http://localhost:3000/api/v2/startAgent', { + body: JSON.stringify({ + channelName: 'test-channel', + rtcUid: 9999, + userUid: 1234, + }), + method: 'POST', + }) + const response = await startAgent(request) + const body = await getJson(response) + + assert(response.status === 200, 'POST /api/v2/startAgent should return 200 on success') + assert(body.code === 0, 'POST /api/v2/startAgent should return code 0') + assert((body.data as Record)?.agent_id === 'mock-agent-id', 'POST /api/v2/startAgent should return the started agent id') + assert(capturedSessionConfig !== null, 'POST /api/v2/startAgent should call createSession') + const sessionConfig = capturedSessionConfig as { + channel?: string + agentUid?: string + remoteUids?: string[] + } + + assert(sessionConfig.channel === 'test-channel', 'POST /api/v2/startAgent should pass the requested channel to createSession') + assert(sessionConfig.agentUid === '9999', 'POST /api/v2/startAgent should stringify the rtc uid for the agent session') + assert(JSON.stringify(sessionConfig.remoteUids) === JSON.stringify(['1234']), 'POST /api/v2/startAgent should scope the session to the requesting user') + } finally { + Agent.prototype.createSession = originalCreateSession + } +} + +async function verifyStopAgentValidation() { + delete process.env.AGENT_BACKEND_URL + + const request = new NextRequest('http://localhost:3000/api/v2/stopAgent', { + body: JSON.stringify({}), + method: 'POST', + }) + const response = await stopAgent(request) + const body = await getJson(response) + + assert(response.status === 400, 'POST /api/v2/stopAgent should reject missing agentId') + assert(body.detail === 'agentId is required', 'POST /api/v2/stopAgent should explain validation failure') +} + +async function verifyStopAgentSuccess() { + process.env.AGORA_APP_ID = '0123456789abcdef0123456789abcdef' + process.env.AGORA_APP_CERTIFICATE = 'fedcba9876543210fedcba9876543210' + delete process.env.AGENT_BACKEND_URL + + const originalStopAgent = AgoraClient.prototype.stopAgent + let stoppedAgentId: string | null = null + + AgoraClient.prototype.stopAgent = (async function (this: AgoraClient, agentId: string) { + stoppedAgentId = agentId + }) as typeof AgoraClient.prototype.stopAgent + + try { + const request = new NextRequest('http://localhost:3000/api/v2/stopAgent', { + body: JSON.stringify({ agentId: 'mock-agent-id' }), + method: 'POST', + }) + const response = await stopAgent(request) + const body = await getJson(response) + + assert(response.status === 200, 'POST /api/v2/stopAgent should return 200 on success') + assert(body.code === 0, 'POST /api/v2/stopAgent should return code 0') + assert(stoppedAgentId === 'mock-agent-id', 'POST /api/v2/stopAgent should call stopAgent with the requested agent id') + } finally { + AgoraClient.prototype.stopAgent = originalStopAgent + } +} + +async function main() { + await verifyGetConfigRoute() + await verifyStartAgentValidation() + await verifyStartAgentSuccess() + await verifyStopAgentValidation() + await verifyStopAgentSuccess() + console.log('API contract checks passed') +} + +await main() diff --git a/web-client/scripts/verify-local-fastapi.ts b/web-client/scripts/verify-local-fastapi.ts new file mode 100644 index 0000000..21c316d --- /dev/null +++ b/web-client/scripts/verify-local-fastapi.ts @@ -0,0 +1,176 @@ +import { existsSync } from 'node:fs' +import path from 'node:path' + +import { NextRequest } from 'next/server' + +import { GET as getConfig } from '../app/api/get_config/route' +import { POST as startAgent } from '../app/api/v2/startAgent/route' +import { POST as stopAgent } from '../app/api/v2/stopAgent/route' + +type BunRuntime = typeof globalThis & { + Bun: { + sleep: (ms: number) => Promise + spawn: (options: { + cmd: string[] + cwd: string + env: Record + stdout: 'ignore' + stderr: 'pipe' + }) => { + kill: () => void + exited: Promise + exitCode: number | null + stderr: ReadableStream | null + } + spawnSync: (options: { + cmd: string[] + cwd: string + stderr: 'pipe' + stdout: 'ignore' + }) => { + exitCode: number + stderr: { toString: () => string } + } + } +} + +const bunRuntime = globalThis as BunRuntime + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message) + } +} + +function getJson(response: Response) { + return response.json() as Promise> +} + +async function waitForHealthyBackend(baseUrl: string, timeoutMs: number) { + const deadline = Date.now() + timeoutMs + let lastError = 'backend did not start' + + while (Date.now() < deadline) { + try { + const response = await fetch(`${baseUrl}/get_config?uid=4321&channel=python-smoke`) + if (response.ok) { + return + } + lastError = `backend returned ${response.status}` + } catch (error) { + lastError = error instanceof Error ? error.message : String(error) + } + + await bunRuntime.Bun.sleep(250) + } + + throw new Error(`Timed out waiting for FastAPI backend: ${lastError}`) +} + +async function main() { + const projectRoot = process.cwd() + const serverRoot = path.resolve(projectRoot, '..', 'server-python') + const venvPython = path.join(serverRoot, 'venv', 'bin', 'python') + + if (!existsSync(venvPython)) { + throw new Error('Missing server-python/venv/bin/python. Run bun run setup:backend before verify:local.') + } + + const dependencyCheck = bunRuntime.Bun.spawnSync({ + cmd: [venvPython, '-c', 'import dotenv, fastapi, uvicorn'], + cwd: serverRoot, + stderr: 'pipe', + stdout: 'ignore', + }) + if (dependencyCheck.exitCode !== 0) { + const stderr = dependencyCheck.stderr.toString().trim() + throw new Error( + `The backend virtualenv is missing required packages. Run bun run setup:backend before verify:local.${stderr ? ` Python said: ${stderr}` : ''}`, + ) + } + + const port = 43120 + Math.floor(Math.random() * 20) + const backendUrl = `http://127.0.0.1:${port}` + const originalBackendUrl = process.env.AGENT_BACKEND_URL + + const serverProcess = bunRuntime.Bun.spawn({ + cmd: [venvPython, 'scripts/run_fake_server.py'], + cwd: serverRoot, + env: { + ...process.env, + AGORA_APP_ID: '0123456789abcdef0123456789abcdef', + AGORA_APP_CERTIFICATE: 'fedcba9876543210fedcba9876543210', + PORT: String(port), + }, + stdout: 'ignore', + stderr: 'pipe', + }) + + try { + await waitForHealthyBackend(backendUrl, 10_000) + + process.env.AGENT_BACKEND_URL = backendUrl + + const response = await getConfig( + new NextRequest('http://localhost:3000/api/get_config?uid=4321&channel=python-smoke'), + ) + const body = await getJson(response) + + assert(response.status === 200, 'GET /api/get_config should proxy to the FastAPI app') + assert(body.code === 0, 'GET /api/get_config should preserve the FastAPI success payload') + + const data = body.data as Record | undefined + assert(data?.uid === '4321', 'GET /api/get_config should preserve the requested uid through FastAPI') + assert(data?.channel_name === 'python-smoke', 'GET /api/get_config should preserve the requested channel through FastAPI') + assert(typeof data?.token === 'string' && data.token.length > 0, 'GET /api/get_config should return a token from FastAPI') + assert(typeof data?.agent_uid === 'string' && data.agent_uid.length > 0, 'GET /api/get_config should return an agent uid from FastAPI') + + const startResponse = await startAgent( + new NextRequest('http://localhost:3000/api/v2/startAgent', { + method: 'POST', + body: JSON.stringify({ + channelName: 'python-smoke', + rtcUid: 9999, + userUid: 4321, + }), + }), + ) + const startBody = await getJson(startResponse) + assert(startResponse.status === 200, 'POST /api/v2/startAgent should proxy to the FastAPI app') + assert(startBody.code === 0, 'POST /api/v2/startAgent should preserve the FastAPI success payload') + assert( + (startBody.data as Record | undefined)?.agent_id === 'fake-agent-9999', + 'POST /api/v2/startAgent should return the agent id from FastAPI', + ) + + const stopResponse = await stopAgent( + new NextRequest('http://localhost:3000/api/v2/stopAgent', { + method: 'POST', + body: JSON.stringify({ agentId: 'fake-agent-9999' }), + }), + ) + const stopBody = await getJson(stopResponse) + assert(stopResponse.status === 200, 'POST /api/v2/stopAgent should proxy to the FastAPI app') + assert(stopBody.code === 0, 'POST /api/v2/stopAgent should preserve the FastAPI success payload') + + console.log('Local FastAPI app proxy smoke check passed') + } finally { + if (originalBackendUrl) { + process.env.AGENT_BACKEND_URL = originalBackendUrl + } else { + delete process.env.AGENT_BACKEND_URL + } + + serverProcess.kill() + await serverProcess.exited + + if (serverProcess.exitCode && serverProcess.exitCode !== 0) { + const stderr = await new Response(serverProcess.stderr).text() + if (stderr.trim()) { + console.error(stderr.trim()) + } + } + } +} + +await main() diff --git a/web-client/scripts/verify-local-proxy.ts b/web-client/scripts/verify-local-proxy.ts new file mode 100644 index 0000000..ed3ae71 --- /dev/null +++ b/web-client/scripts/verify-local-proxy.ts @@ -0,0 +1,141 @@ +import { NextRequest } from 'next/server' + +import { GET as getConfig } from '../app/api/get_config/route' +import { POST as startAgent } from '../app/api/v2/startAgent/route' +import { POST as stopAgent } from '../app/api/v2/stopAgent/route' + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message) + } +} + +function getJson(response: Response) { + return response.json() as Promise> +} + +type LocalServer = { + port: number + stop: (closeActiveConnections?: boolean) => void +} + +async function withStubBackend(run: (baseUrl: string) => Promise) { + const bunRuntime = globalThis as typeof globalThis & { + Bun: { + serve: (options: { + port: number + fetch: (request: Request) => Promise | Response + }) => LocalServer + } + } + + const handler = async (request: Request) => { + const url = new URL(request.url) + + if (request.method === 'GET' && url.pathname === '/get_config') { + return Response.json({ + code: 0, + data: { + app_id: 'stub-app-id', + token: 'stub-token', + uid: '4321', + channel_name: 'proxy-channel', + agent_uid: '9999', + }, + msg: 'success', + }) + } + + if (request.method === 'POST' && url.pathname === '/v2/startAgent') { + const parsedBody = await request.json() as { rtcUid?: number; userUid?: number } + if (parsedBody.rtcUid !== 9999 || parsedBody.userUid !== 4321) { + return Response.json({ detail: 'unexpected proxied payload' }, { status: 400 }) + } + + return Response.json({ + code: 0, + data: { + agent_id: 'agent-proxied', + channel_name: 'proxy-channel', + status: 'started', + }, + msg: 'success', + }) + } + + if (request.method === 'POST' && url.pathname === '/v2/stopAgent') { + return Response.json({ code: 0, msg: 'success' }) + } + + return new Response('not found', { status: 404 }) + } + + let server: LocalServer | null = null + const startPort = 43100 + for (let port = startPort; port < startPort + 20; port += 1) { + try { + server = bunRuntime.Bun.serve({ port, fetch: handler }) + break + } catch {} + } + + if (!server) { + throw new Error('Failed to start stub backend on a local port') + } + + try { + return await run(`http://localhost:${server.port}`) + } finally { + server.stop(true) + } +} + +async function main() { + const originalBackendUrl = process.env.AGENT_BACKEND_URL + + await withStubBackend(async (backendUrl) => { + process.env.AGENT_BACKEND_URL = backendUrl + + const configResponse = await getConfig( + new NextRequest('http://localhost:3000/api/get_config?uid=4321&channel=proxy-channel'), + ) + const configBody = await getJson(configResponse) + assert(configResponse.status === 200, 'GET /api/get_config should proxy successfully') + assert(configBody.code === 0, 'GET /api/get_config should preserve proxied success payload') + assert((configBody.data as Record)?.token === 'stub-token', 'GET /api/get_config should return proxied token') + + const startResponse = await startAgent( + new NextRequest('http://localhost:3000/api/v2/startAgent', { + method: 'POST', + body: JSON.stringify({ + channelName: 'proxy-channel', + rtcUid: 9999, + userUid: 4321, + }), + }), + ) + const startBody = await getJson(startResponse) + assert(startResponse.status === 200, 'POST /api/v2/startAgent should proxy successfully') + assert((startBody.data as Record)?.agent_id === 'agent-proxied', 'POST /api/v2/startAgent should return proxied agent id') + + const stopResponse = await stopAgent( + new NextRequest('http://localhost:3000/api/v2/stopAgent', { + method: 'POST', + body: JSON.stringify({ agentId: 'agent-proxied' }), + }), + ) + const stopBody = await getJson(stopResponse) + assert(stopResponse.status === 200, 'POST /api/v2/stopAgent should proxy successfully') + assert(stopBody.code === 0, 'POST /api/v2/stopAgent should preserve proxied success payload') + }) + + if (originalBackendUrl) { + process.env.AGENT_BACKEND_URL = originalBackendUrl + } else { + delete process.env.AGENT_BACKEND_URL + } + + console.log('Local proxy checks passed') +} + +await main() diff --git a/web-client/src/components/app.tsx b/web-client/src/components/app.tsx index 38d9388..dae05eb 100644 --- a/web-client/src/components/app.tsx +++ b/web-client/src/components/app.tsx @@ -1,270 +1,137 @@ -import { LogPanel } from '@/components/log-panel' -import { SubtitlePanel } from '@/components/subtitle-panel' +'use client' + import { useAgoraConnection } from '@/hooks/useAgoraConnection' import { cn } from '@/lib/utils' -import { useAppStore } from '@/stores/app-store' -import { AgentState } from 'agora-agent-client-toolkit' - -const stateLabels: Record = { - [AgentState.IDLE]: 'Ready', - [AgentState.LISTENING]: 'Listening', - [AgentState.THINKING]: 'Thinking', - [AgentState.SPEAKING]: 'Speaking', - [AgentState.SILENT]: 'Silent', -} - -const stateAccent: Record = { - [AgentState.IDLE]: 'bg-slate-400', - [AgentState.LISTENING]: 'bg-emerald-400', - [AgentState.THINKING]: 'bg-amber-400', - [AgentState.SPEAKING]: 'bg-cyan-400', - [AgentState.SILENT]: 'bg-slate-500', -} +import { AgentVisualizer, ConvoTextStream } from 'agora-agent-uikit' +import { MicButtonWithVisualizer } from 'agora-agent-uikit/rtc' export default function App() { - const isConnected = useAppStore((s) => s.isConnected) - const isConnecting = useAppStore((s) => s.isConnecting) - const isMicMuted = useAppStore((s) => s.isMicMuted) - const agentState = useAppStore((s) => s.agentState) - const channelName = useAppStore((s) => s.channelName) - const agentId = useAppStore((s) => s.agentId) - const transcriptCount = useAppStore((s) => s.transcripts.length) - const logCount = useAppStore((s) => s.logs.length) - const { connect, disconnect, toggleMicrophone } = useAgoraConnection() - - const handleStartAgent = async () => { - if (isConnecting) return - try { - await connect() - } catch {} - } - - const handleStopAgent = async () => { - await disconnect() - } - - const handleToggleMic = () => { - toggleMicrophone() - } - - const sessionStatus = isConnected ? 'Connected' : isConnecting ? 'Connecting...' : 'Standby' - const orbTone = isConnecting - ? 'border-cyan-400/40 bg-cyan-400/10' - : isConnected - ? 'border-emerald-400/30 bg-emerald-400/10' - : 'border-[hsl(var(--border))] bg-[hsl(var(--muted))]' + const { + RemoteUser, + agentId, + agentUid, + channelName, + connect, + currentInProgressMessage, + disconnect, + error, + isConnected, + isConnecting, + isMicEnabled, + localMicrophoneTrack, + messageList, + remoteUsers, + setMicEnabled, + toggleMicrophone, + visualizerState, + } = useAgoraConnection() return ( -
-
-
-
-
-

- Agora Conversational AI -

-

Python Voice Agent Quickstart

-
- -
- - Session {sessionStatus} - - - Transcript {transcriptCount} - - - Logs {logCount} - -
-
-
- +
+
) } diff --git a/web-client/src/components/control-bar.tsx b/web-client/src/components/control-bar.tsx deleted file mode 100644 index 6632a1c..0000000 --- a/web-client/src/components/control-bar.tsx +++ /dev/null @@ -1,154 +0,0 @@ -'use client' - -import { cn } from '@/lib/utils' -import { useAppStore } from '@/stores/app-store' -import { AgentState } from 'agora-agent-client-toolkit' - -interface ControlBarProps { - onStartAgent: () => void - onStopAgent: () => void - onToggleMic: () => void -} - -const stateLabels: Record = { - [AgentState.IDLE]: 'Idle', - [AgentState.LISTENING]: 'Listening', - [AgentState.THINKING]: 'Thinking', - [AgentState.SPEAKING]: 'Speaking', - [AgentState.SILENT]: 'Silent', -} - -const stateColors: Record = { - [AgentState.IDLE]: 'bg-slate-500', - [AgentState.LISTENING]: 'bg-emerald-500', - [AgentState.THINKING]: 'bg-amber-500', - [AgentState.SPEAKING]: 'bg-blue-500', - [AgentState.SILENT]: 'bg-slate-600', -} - -export function ControlBar({ onStartAgent, onStopAgent, onToggleMic }: ControlBarProps) { - const isConnected = useAppStore((s) => s.isConnected) - const isConnecting = useAppStore((s) => s.isConnecting) - const isMicMuted = useAppStore((s) => s.isMicMuted) - const agentState = useAppStore((s) => s.agentState) - - return ( -
-
- {isConnected && ( -
- - Agent: {stateLabels[agentState]} -
- )} -
- -
- {isConnected && ( - - )} - - {!isConnected ? ( - - ) : ( - - )} -
-
- ) -} diff --git a/web-client/src/components/log-panel.tsx b/web-client/src/components/log-panel.tsx deleted file mode 100644 index 0951c1b..0000000 --- a/web-client/src/components/log-panel.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client' - -import { cn, formatTime } from '@/lib/utils' -import { type LogItem, type LogLevel, useAppStore } from '@/stores/app-store' -import { useEffect, useRef, useState } from 'react' - -const levelStyles: Record = { - info: 'text-slate-300', - success: 'text-emerald-300', - error: 'text-red-300', - warning: 'text-amber-300', -} - -const levelIcons: Record = { - info: 'i', - success: '+', - error: '!', - warning: '~', -} - -function LogRow({ item }: { item: LogItem }) { - return ( -
- {formatTime(item.timestamp)} - {levelIcons[item.level]} - {item.message} -
- ) -} - -export function LogPanel() { - const logs = useAppStore((s) => s.logs) - const clearLogs = useAppStore((s) => s.clearLogs) - const scrollRef = useRef(null) - const [isCollapsed, setIsCollapsed] = useState(false) - - useEffect(() => { - if (scrollRef.current && !isCollapsed) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight - } - }, [logs, isCollapsed]) - - return ( -
-
-
-

Runtime logs

- ({logs.length}) -
- -
- - - -
-
- - {!isCollapsed && ( -
- {logs.length === 0 ? ( -
- No runtime events yet. -
- ) : ( - logs.map((item) => ) - )} -
- )} -
- ) -} diff --git a/web-client/src/components/subtitle-panel.tsx b/web-client/src/components/subtitle-panel.tsx deleted file mode 100644 index bc04f18..0000000 --- a/web-client/src/components/subtitle-panel.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client' - -import { cn } from '@/lib/utils' -import { type TranscriptItem, useAppStore } from '@/stores/app-store' -import { TurnStatus } from 'agora-agent-client-toolkit' -import { useEffect, useRef } from 'react' - -function TranscriptRow({ item }: { item: TranscriptItem }) { - const isAgent = item.type === 'agent' - const isInProgress = item.status === TurnStatus.IN_PROGRESS - - return ( -
-
- - {isAgent ? 'Agent' : 'You'} - - -
-

- {item.text} - {isInProgress && } -

-
-
-
- ) -} - -export function SubtitlePanel() { - const transcripts = useAppStore((s) => s.transcripts) - const scrollRef = useRef(null) - - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight - } - }, [transcripts]) - - return ( -
-
-
-

Transcript

- {transcripts.length} turns -
-
- -
- {transcripts.length === 0 ? ( -
- Waiting for the first utterance. -
- ) : ( - transcripts.map((item) => ) - )} -
-
- ) -} diff --git a/web-client/src/hooks/useAgoraConnection.ts b/web-client/src/hooks/useAgoraConnection.ts index 98e3755..567a5ef 100644 --- a/web-client/src/hooks/useAgoraConnection.ts +++ b/web-client/src/hooks/useAgoraConnection.ts @@ -1,12 +1,21 @@ 'use client' import { getConfig, startAgent, stopAgent } from '@/services/api' -import { type TranscriptItem, useAppStore } from '@/stores/app-store' -import { AgoraVoiceAI, AgoraVoiceAIEvents } from 'agora-agent-client-toolkit' -import type { AgentTranscription, TranscriptHelperItem, UserTranscription } from 'agora-agent-client-toolkit' import { + getCurrentInProgressMessage, + getMessageList, + mapAgentVisualizerState, + normalizeTranscript, +} from '@/lib/conversation' +import { AgoraVoiceAI, AgoraVoiceAIEvents, type AgentState } from 'agora-agent-client-toolkit' +import type { + AgentTranscription, + TranscriptHelperItem, + UserTranscription, +} from 'agora-agent-client-toolkit' +import { + RemoteUser, useClientEvent, - useIsConnected, useJoin, useLocalMicrophoneTrack, usePublish, @@ -14,7 +23,8 @@ import { useRemoteUsers, } from 'agora-rtc-react' import AgoraRTM, { type RTMClient } from 'agora-rtm' -import { useCallback, useEffect, useRef, useState } from 'react' +import { setParameter } from 'agora-rtc-sdk-ng/esm' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' interface ConnectionConfig { appId: string @@ -26,30 +36,48 @@ interface ConnectionConfig { export function useAgoraConnection() { const client = useRTCClient() - const isRtcConnected = useIsConnected() const remoteUsers = useRemoteUsers() const [config, setConfig] = useState(null) const [shouldJoin, setShouldJoin] = useState(false) - const [micEnabled, setMicEnabled] = useState(true) + const [isReady, setIsReady] = useState(false) + const [isConnecting, setIsConnecting] = useState(false) + const [error, setError] = useState(null) + const [isMicEnabled, setIsMicEnabled] = useState(true) + const [agentState, setAgentState] = useState(null) + const [rawTranscript, setRawTranscript] = useState< + TranscriptHelperItem>[] + >([]) + const [connectionState, setConnectionState] = useState('DISCONNECTED') + const [agentId, setAgentId] = useState(null) - const rtmClientRef = useRef(null) const currentAgentIdRef = useRef(null) + const rtmClientRef = useRef(null) const voiceAIRef = useRef(null) + const initKeyRef = useRef(null) - const addLog = useCallback((message: string, level: 'info' | 'success' | 'error' | 'warning' = 'info') => { - useAppStore.getState().addLog(message, level) + useEffect(() => { + let cancelled = false + const timeoutId = setTimeout(() => { + if (!cancelled) setIsReady(true) + }, 0) + + return () => { + cancelled = true + clearTimeout(timeoutId) + setIsReady(false) + } }, []) - // Local microphone track - const { localMicrophoneTrack } = useLocalMicrophoneTrack(micEnabled && shouldJoin, { - AEC: true, - ANS: false, - AGC: true, - }) + useEffect(() => { + if (!client) return - // Join channel when config is ready - const { isConnected: joinSuccess } = useJoin( + try { + setParameter('ENABLE_AUDIO_PTS', true) + } catch {} + }, [client]) + + const { isConnected } = useJoin( config ? { appid: config.appId, @@ -58,185 +86,147 @@ export function useAgoraConnection() { uid: config.uid, } : { appid: '', channel: '', token: null, uid: 0 }, - shouldJoin && !!config, + isReady && shouldJoin && !!config, ) - // Publish local track + const { localMicrophoneTrack } = useLocalMicrophoneTrack(isReady && shouldJoin) + usePublish([localMicrophoneTrack]) - // Handle remote user audio useClientEvent(client, 'user-published', async (user, mediaType) => { - if (mediaType === 'audio') { - await client.subscribe(user, mediaType) - user.audioTrack?.play() - } + if (mediaType !== 'audio') return + await client.subscribe(user, mediaType) + user.audioTrack?.play() }) - useClientEvent(client, 'user-joined', (user) => { - addLog(`User joined: ${user.uid}`, 'info') + useClientEvent(client, 'connection-state-change', (currentState) => { + setConnectionState(currentState) }) - useClientEvent(client, 'connection-state-change', (curState, _prevState, reason) => { - if (curState === 'CONNECTED') { - addLog('RTC connected successfully', 'success') - } else if (curState === 'DISCONNECTED') { - addLog(`RTC disconnected: ${reason || 'Unknown reason'}`, 'warning') + const cleanup = useCallback(async () => { + initKeyRef.current = null + + if (voiceAIRef.current) { + try { + voiceAIRef.current.unsubscribe() + voiceAIRef.current.destroy() + } catch {} + voiceAIRef.current = null } - }) - // Initialize RTM + AgoraVoiceAI + Agent after RTC join success + if (rtmClientRef.current) { + try { + await rtmClientRef.current.logout() + } catch {} + rtmClientRef.current = null + } + }, []) + useEffect(() => { - if (!joinSuccess || !config || !client) return + if (!isReady || !isConnected || !config || !client) return + + const initKey = `${config.channel}:${config.uid}` + if (initKeyRef.current === initKey) return + initKeyRef.current = initKey + + let cancelled = false - const initAll = async () => { + const initializeSession = async () => { try { - // Initialize RTM - addLog('Initializing RTM Client...', 'info') const rtmClient = new AgoraRTM.RTM(config.appId, String(config.uid)) - rtmClientRef.current = rtmClient - addLog('RTM Client initialized successfully', 'success') - - // RTM Login - addLog('Logging in to RTM...', 'info') - try { - await rtmClient.login({ token: config.token }) - addLog('RTM login successful', 'success') - } catch (e: unknown) { - const error = e as { code?: number } - if (error.code === -10017) { - addLog('RTM already logged in', 'success') - } else throw e - } - - // RTM Subscribe - addLog('Subscribing to RTM channel...', 'info') + await rtmClient.login({ token: config.token }) await rtmClient.subscribe(config.channel) - addLog('Subscribed to RTM channel successfully', 'success') + rtmClientRef.current = rtmClient - // Initialize AgoraVoiceAI (imperative, no Provider needed) - addLog('Initializing ConvoAI API...', 'info') const voiceAI = await AgoraVoiceAI.init({ - rtcEngine: client as any, + rtcEngine: client, rtmConfig: { rtmEngine: rtmClient }, enableLog: true, }) - voiceAIRef.current = voiceAI - setupVoiceAIEvents(voiceAI) + + if (cancelled) { + voiceAI.unsubscribe() + voiceAI.destroy() + return + } + + voiceAI.on(AgoraVoiceAIEvents.TRANSCRIPT_UPDATED, (transcript) => { + setRawTranscript([...transcript]) + }) + voiceAI.on(AgoraVoiceAIEvents.AGENT_STATE_CHANGED, (_agentUserId, event) => { + setAgentState(event.state) + }) voiceAI.subscribeMessage(config.channel) - addLog('ConvoAI API initialized successfully', 'success') - - // Start Agent - addLog('Starting agent...', 'info') - const agentId = await startAgent(config.channel, String(config.agentUid), String(config.uid)) - currentAgentIdRef.current = agentId - useAppStore.getState().setAgentId(agentId) - addLog(`Agent started successfully (ID: ${agentId})`, 'success') - - useAppStore.getState().setIsConnected(true) - useAppStore.getState().setIsConnecting(false) - } catch (error) { - const err = error as Error - addLog(`Connection failed: ${err.message}`, 'error') - useAppStore.getState().setIsConnecting(false) + voiceAIRef.current = voiceAI + + const nextAgentId = await startAgent(config.channel, config.agentUid, config.uid) + currentAgentIdRef.current = nextAgentId + setAgentId(nextAgentId) + setError(null) + } catch (nextError) { + initKeyRef.current = null + setError(nextError instanceof Error ? nextError.message : 'Failed to start conversation') + setShouldJoin(false) await cleanup() + } finally { + setIsConnecting(false) } } - initAll() - }, [joinSuccess, config, client, addLog]) - - const setupVoiceAIEvents = (ai: AgoraVoiceAI) => { - ai.on( - AgoraVoiceAIEvents.TRANSCRIPT_UPDATED, - (chatHistory: TranscriptHelperItem>[]) => { - const transcripts: TranscriptItem[] = chatHistory - .sort((a, b) => { - if (a.turn_id !== b.turn_id) return a.turn_id - b.turn_id - return Number(a.uid) - Number(b.uid) - }) - .map((item) => ({ - id: `${item.turn_id}-${item.uid}-${item._time}`, - type: Number(item.uid) !== 0 ? 'agent' : 'user', - text: item.text || '', - status: item.status, - timestamp: item._time || Date.now(), - })) - useAppStore.getState().setTranscripts(transcripts) - }, - ) - - ai.on(AgoraVoiceAIEvents.AGENT_STATE_CHANGED, (_agentUserId: string, event) => { - useAppStore.getState().setAgentState(event.state) - }) - - ai.on(AgoraVoiceAIEvents.AGENT_ERROR, (_agentUserId: string, error) => { - addLog(`Agent error: [${error.type}] ${error.message} (code: ${error.code})`, 'error') - }) - - ai.on(AgoraVoiceAIEvents.MESSAGE_ERROR, (_agentUserId: string, error) => { - addLog(`Message error: [${error.type}] ${error.message} (code: ${error.code})`, 'error') - }) - } + initializeSession() - const cleanup = async () => { - if (voiceAIRef.current) { - try { - voiceAIRef.current.unsubscribe() - voiceAIRef.current.destroy() - } catch {} - voiceAIRef.current = null + return () => { + cancelled = true } + }, [cleanup, client, config, isConnected, isReady]) - if (rtmClientRef.current) { - try { - await rtmClientRef.current.logout() - } catch {} - rtmClientRef.current = null + const renewAgoraTokens = useCallback(async () => { + if (!config || !client) return + + try { + const rtcConfig = await getConfig({ channel: config.channel, uid: String(config.uid) }) + const rtmConfig = await getConfig({ channel: config.channel, uid: '0' }) + await client.renewToken(rtcConfig.token) + await rtmClientRef.current?.renewToken(rtmConfig.token) + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : 'Failed to renew Agora token') } - } + }, [client, config]) + + useClientEvent(client, 'token-privilege-will-expire', renewAgoraTokens) const connect = useCallback(async () => { - const storeState = useAppStore.getState() - storeState.setIsConnecting(true) - storeState.clearLogs() + setIsConnecting(true) + setError(null) + setAgentState(null) + setRawTranscript([]) + setAgentId(null) + currentAgentIdRef.current = null + setIsMicEnabled(true) try { - addLog('Getting configuration...', 'info') const configData = await getConfig() - - const connectionConfig: ConnectionConfig = { + setConfig({ appId: configData.app_id, channel: configData.channel_name, token: configData.token, uid: Number(configData.uid), agentUid: Number(configData.agent_uid), - } - - storeState.setChannelName(connectionConfig.channel) - addLog('Configuration retrieved successfully', 'success') - - addLog('Initializing RTC Engine...', 'info') - setConfig(connectionConfig) + }) setShouldJoin(true) - setMicEnabled(true) - addLog('RTC Engine initialized successfully', 'success') - addLog('Joining RTC channel...', 'info') - } catch (error) { - const err = error as Error - addLog(`Failed to get config: ${err.message}`, 'error') - storeState.setIsConnecting(false) + } catch (nextError) { + setIsConnecting(false) + setError(nextError instanceof Error ? nextError.message : 'Failed to initialize conversation') } - }, [addLog]) + }, []) const disconnect = useCallback(async () => { - if (currentAgentIdRef.current && config?.channel) { + setIsConnecting(false) + + if (currentAgentIdRef.current) { try { - await stopAgent(config.channel, currentAgentIdRef.current) - addLog('Agent stopped', 'success') - } catch (e) { - const err = e as Error - addLog(`Failed to stop agent: ${err.message}`, 'error') - } + await stopAgent(currentAgentIdRef.current) + } catch {} currentAgentIdRef.current = null } @@ -249,27 +239,67 @@ export function useAgoraConnection() { setShouldJoin(false) setConfig(null) + setRawTranscript([]) + setAgentState(null) + setAgentId(null) + setError(null) + setConnectionState('DISCONNECTED') + }, [cleanup, localMicrophoneTrack]) + + const toggleMicrophone = useCallback(async () => { + const nextEnabled = !isMicEnabled + if (!localMicrophoneTrack) { + setIsMicEnabled(nextEnabled) + return + } - useAppStore.getState().reset() - addLog('Disconnected', 'info') - }, [config, localMicrophoneTrack, addLog]) + try { + await localMicrophoneTrack.setEnabled(nextEnabled) + setIsMicEnabled(nextEnabled) + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : 'Failed to update microphone state') + } + }, [isMicEnabled, localMicrophoneTrack]) - const toggleMicrophone = useCallback(() => { - const storeState = useAppStore.getState() - const newMuted = !storeState.isMicMuted + const normalizedTranscript = useMemo(() => { + if (!config || client.uid == null) return [] + return normalizeTranscript(rawTranscript, String(client.uid)) + }, [client.uid, config, rawTranscript]) - if (localMicrophoneTrack) { - localMicrophoneTrack.setMuted(newMuted) - } - storeState.setIsMicMuted(newMuted) - addLog(newMuted ? 'Microphone muted' : 'Microphone unmuted', 'info') - }, [localMicrophoneTrack, addLog]) + const messageList = useMemo(() => getMessageList(normalizedTranscript), [normalizedTranscript]) + const currentInProgressMessage = useMemo( + () => getCurrentInProgressMessage(normalizedTranscript), + [normalizedTranscript], + ) + + const isAgentConnected = useMemo(() => { + if (!config) return false + return remoteUsers.some((user) => String(user.uid) === String(config.agentUid)) + }, [config, remoteUsers]) + + const visualizerState = useMemo( + () => mapAgentVisualizerState(agentState, isAgentConnected, connectionState), + [agentState, connectionState, isAgentConnected], + ) return { + RemoteUser, + agentId, + agentUid: config ? String(config.agentUid) : null, + channelName: config?.channel ?? '', connect, + currentInProgressMessage, disconnect, - toggleMicrophone, - isConnected: isRtcConnected, + error, + isAgentConnected, + isConnected, + isConnecting, + isMicEnabled, + localMicrophoneTrack, + messageList, remoteUsers, + setMicEnabled: setIsMicEnabled, + toggleMicrophone, + visualizerState, } } diff --git a/web-client/src/index.css b/web-client/src/index.css index 398d8fc..137cb7a 100644 --- a/web-client/src/index.css +++ b/web-client/src/index.css @@ -4,28 +4,32 @@ :root { color-scheme: dark; - --background: 222 28% 8%; - --foreground: 210 20% 94%; - --card: 222 24% 11%; - --card-foreground: 210 20% 94%; - --popover: 222 24% 11%; - --popover-foreground: 210 20% 94%; - --muted: 222 18% 15%; - --muted-foreground: 215 14% 63%; - --primary: 194 93% 48%; - --primary-foreground: 222 28% 8%; - --secondary: 222 17% 17%; - --secondary-foreground: 210 20% 94%; - --accent: 194 93% 48%; - --accent-foreground: 222 28% 8%; - --destructive: 0 70% 55%; + --background: 220 14% 5%; + --surface: 220 12% 9%; + --surface-elevated: 220 10% 13%; + --card: 220 12% 9%; + --card-foreground: 220 15% 95%; + --popover: 220 10% 13%; + --popover-foreground: 220 15% 95%; + --muted: 220 12% 9%; + --foreground: 220 15% 95%; + --muted-foreground: 220 10% 50%; + --primary: 194 100% 50%; + --primary-foreground: 220 14% 5%; + --secondary: 220 10% 13%; + --secondary-foreground: 220 15% 95%; + --accent: 194 100% 50%; + --accent-foreground: 220 14% 5%; + --destructive: 0 72% 58%; --destructive-foreground: 0 0% 100%; - --border: 217 18% 22%; - --input: 217 18% 22%; - --ring: 194 93% 48%; - --surface-subtle: 222 20% 10%; + --border: 220 10% 18%; + --input: 220 10% 18%; + --ring: 194 100% 50%; + --radius: 0.5rem; + --viz-stop-1: 194 100% 58%; + --viz-stop-2: 215 42% 72%; + --viz-stop-3: 272 62% 72%; --font-sans: "Instrument Sans", "Inter", system-ui, sans-serif; - --font-mono: "Space Mono", "SFMono-Regular", ui-monospace, monospace; } * { @@ -33,39 +37,18 @@ } html, -body, -#root { +body { min-height: 100%; } body { margin: 0; - min-height: 100vh; font-family: var(--font-sans); - background: hsl(var(--background)); color: hsl(var(--foreground)); + background-color: hsl(var(--background)); } -::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -::-webkit-scrollbar-track { - background: hsl(var(--card)); - border-radius: 3px; -} - -::-webkit-scrollbar-thumb { - background: hsl(var(--border)); - border-radius: 3px; -} - -::-webkit-scrollbar-thumb:hover { - background: hsl(var(--muted-foreground)); -} - -@keyframes surface-in { +@keyframes fade-up { from { opacity: 0; transform: translateY(8px); @@ -76,6 +59,124 @@ body { } } -.animate-surface-in { - animation: surface-in 220ms ease-out both; +.animate-fade-up { + animation: fade-up 300ms cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.animate-fade-up-d1 { + animation-delay: 60ms; +} + +.animate-fade-up-d2 { + animation-delay: 120ms; +} + +.animate-fade-up-d3 { + animation-delay: 180ms; +} + +#chatbox { + width: 24rem !important; + left: auto !important; + bottom: 8rem !important; + background: transparent !important; + backdrop-filter: none !important; + border: none !important; + border-radius: 0 !important; +} + +#chatbox:has(> .chatbox.expanded) { + background-color: hsl(var(--card) / 0.95) !important; + backdrop-filter: blur(8px) !important; + border: 1px solid hsl(var(--border)) !important; + border-radius: 0.75rem !important; +} + +#chatbox:not(:has(> .chatbox.expanded)) { + display: flex !important; + justify-content: flex-end !important; +} + +#chatbox button[aria-label="Open transcription"] { + border: none !important; +} + +#chatbox .chatbox.expanded { + background-color: transparent !important; +} + +#chatbox .chatbox.expanded h3, +#chatbox .chatbox.expanded button[aria-label="Close transcription"] { + color: hsl(var(--foreground)) !important; +} + +#chatbox .flex.w-full.items-start.gap-2.flex-row > div.flex.h-8.w-8.flex-shrink-0 { + box-sizing: border-box !important; + background-color: hsl(var(--primary) / 0.15) !important; + border: 2px solid hsl(var(--primary)) !important; + color: hsl(var(--primary)) !important; +} + +#chatbox .flex.w-full.items-start.gap-2.flex-row > .flex.flex-col > .rounded-\[15px\] { + color: hsl(var(--primary)) !important; +} + +#chatbox .flex.w-full.items-start.gap-2.flex-row > .flex.flex-col > .rounded-\[15px\] *, +#chatbox .flex.w-full.items-start.gap-2.flex-row-reverse > .flex.flex-col > .rounded-\[15px\] * { + color: inherit !important; +} + +#chatbox .flex.w-full.items-start.gap-2.flex-row-reverse > div.flex.h-8.w-8.flex-shrink-0 { + box-sizing: border-box !important; + background-color: hsl(var(--muted)) !important; + border: 1px solid hsl(var(--border)) !important; + color: hsl(var(--foreground)) !important; +} + +#chatbox .flex.w-full.items-start.gap-2.flex-row-reverse > .flex.flex-col > .rounded-\[15px\] { + background-color: hsl(var(--muted)) !important; + color: hsl(var(--foreground)) !important; +} + +.chatbox { + height: 300px; + transition: height 0.3s ease-in-out; +} + +.chatbox.expanded { + height: 600px; + overflow: hidden; +} + +.chatbox.expanded > div:last-child { + min-height: 0; +} + +.conversation-mic-host { + flex-shrink: 0; +} + +.conversation-mic-host > button { + aspect-ratio: 1 / 1; + width: 3rem; + height: 3rem; + min-width: 3rem; + min-height: 3rem; + max-width: 3rem; + max-height: 3rem; + border-radius: 9999px; + padding: 0; + box-sizing: border-box; + overflow: visible; +} + +@media (min-width: 640px) { + .conversation-mic-host > button { + width: 3.5rem; + height: 3.5rem; + min-width: 3.5rem; + min-height: 3.5rem; + max-width: 3.5rem; + max-height: 3.5rem; + } } diff --git a/web-client/src/lib/conversation.ts b/web-client/src/lib/conversation.ts new file mode 100644 index 0000000..e23f825 --- /dev/null +++ b/web-client/src/lib/conversation.ts @@ -0,0 +1,97 @@ +import { + type AgentState, + type AgentTranscription, + TurnStatus, + type TranscriptHelperItem, + type UserTranscription, +} from 'agora-agent-client-toolkit' +import { + type AgentVisualizerState, + type IMessageListItem, +} from 'agora-agent-uikit' + +export function normalizeTranscriptSpacing(text: string): string { + return text + .replace(/([.!?])([A-Za-z])/g, '$1 $2') + .replace(/,([A-Za-z])/g, ', $1') + .replace(/\s{2,}/g, ' ') + .trim() +} + +export function normalizeTimestampMs(timestamp: number): number { + return timestamp > 1e12 ? timestamp : timestamp * 1000 +} + +export function mapAgentVisualizerState( + agentState: AgentState | null, + isAgentConnected: boolean, + connectionState: string, +): AgentVisualizerState { + if (connectionState === 'DISCONNECTED' || connectionState === 'DISCONNECTING') { + return 'disconnected' + } + + if (connectionState === 'CONNECTING' || connectionState === 'RECONNECTING') { + return 'joining' + } + + if (!isAgentConnected) { + return 'not-joined' + } + + switch (agentState) { + case 'listening': + return 'listening' + case 'thinking': + return 'analyzing' + case 'speaking': + return 'talking' + case 'idle': + case 'silent': + default: + return 'ambient' + } +} + +function toMessageListItem( + item: TranscriptHelperItem>, +): IMessageListItem { + return { + turn_id: item.turn_id, + uid: Number(item.uid) || 0, + text: typeof item.text === 'string' ? item.text : '', + status: item.status as unknown as IMessageListItem['status'], + createdAt: + typeof item._time === 'number' + ? normalizeTimestampMs(item._time) + : undefined, + } +} + +export function normalizeTranscript( + transcript: TranscriptHelperItem>[], + localUid: string, +) { + return transcript.map((item) => { + const nextUid = item.uid === '0' ? localUid : item.uid + const nextText = + typeof item.text === 'string' ? normalizeTranscriptSpacing(item.text) : item.text + + return { ...item, uid: nextUid, text: nextText } + }) +} + +export function getMessageList( + transcript: TranscriptHelperItem>[], +) { + return transcript + .filter((item) => item.status !== TurnStatus.IN_PROGRESS) + .map(toMessageListItem) +} + +export function getCurrentInProgressMessage( + transcript: TranscriptHelperItem>[], +) { + const item = transcript.find((entry) => entry.status === TurnStatus.IN_PROGRESS) + return item ? toMessageListItem(item) : null +} diff --git a/web-client/src/lib/server/agora.ts b/web-client/src/lib/server/agora.ts new file mode 100644 index 0000000..c537e32 --- /dev/null +++ b/web-client/src/lib/server/agora.ts @@ -0,0 +1,176 @@ +import { + AgoraClient, + Agent, + Area, + DeepgramSTT, + ExpiresIn, + MiniMaxTTS, + OpenAI, +} from 'agora-agent-server-sdk' +import { RtcTokenBuilder, RtcRole } from 'agora-token' + +const ADA_PROMPT = `You are Ada, an agentic developer advocate from Agora. You help developers understand and build with Agora's Conversational AI platform. + +Agora is a real-time communications company. The product you represent is the Agora Conversational AI Engine. + +If you do not know a specific fact about Agora, say so plainly and suggest checking docs.agora.io. Keep most replies to one or two sentences unless the user explicitly asks for more detail.` + +export function requireEnv(name: string): string { + const value = process.env[name] + if (!value) { + throw new Error(`Missing required environment variable: ${name}`) + } + return value +} + +export function getAgoraCredentials() { + return { + appId: requireEnv('AGORA_APP_ID'), + appCertificate: requireEnv('AGORA_APP_CERTIFICATE'), + } +} + +export function getAgentGreeting() { + return ( + process.env.AGENT_GREETING ?? + "Hi there! I'm Ada, your virtual assistant from Agora. How can I help?" + ) +} + +export function generateChannelName() { + return `ai-conversation-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` +} + +export function generateRtcAndRtmToken(channelName: string, uid: number) { + const { appId, appCertificate } = getAgoraCredentials() + const expirationTime = Math.floor(Date.now() / 1000) + 3600 + + return RtcTokenBuilder.buildTokenWithRtm( + appId, + appCertificate, + channelName, + uid, + RtcRole.PUBLISHER, + expirationTime, + expirationTime, + ) +} + +export function createAgoraClient() { + const { appId, appCertificate } = getAgoraCredentials() + + return new AgoraClient({ + area: Area.US, + appId, + appCertificate, + }) +} + +export function createManagedAgent() { + const greeting = getAgentGreeting() + + return new Agent({ + name: `conversation-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + instructions: ADA_PROMPT, + greeting, + failureMessage: 'Please wait a moment.', + maxHistory: 50, + turnDetection: { + config: { + speech_threshold: 0.5, + start_of_speech: { + mode: 'vad', + vad_config: { + interrupt_duration_ms: 160, + prefix_padding_ms: 300, + }, + }, + end_of_speech: { + mode: 'vad', + vad_config: { + silence_duration_ms: 480, + }, + }, + }, + }, + advancedFeatures: { enable_rtm: true, enable_tools: true }, + parameters: { data_channel: 'rtm', enable_error_message: true }, + }) + .withStt( + new DeepgramSTT({ + model: 'nova-3', + language: 'en', + }), + ) + .withLlm( + new OpenAI({ + model: 'gpt-4o-mini', + greetingMessage: greeting, + failureMessage: 'Please wait a moment.', + maxHistory: 15, + params: { + max_tokens: 1024, + temperature: 0.7, + top_p: 0.95, + }, + }), + ) + .withTts( + new MiniMaxTTS({ + model: 'speech_2_6_turbo', + voiceId: 'English_captivating_female1', + }), + ) +} + +export function getAgentBackendUrl() { + return process.env.AGENT_BACKEND_URL?.replace(/\/$/, '') ?? null +} + +export async function proxyToPythonBackend( + path: string, + init?: RequestInit, + searchParams?: URLSearchParams, +) { + const backendUrl = getAgentBackendUrl() + if (!backendUrl) { + return null + } + + const target = new URL(path, `${backendUrl}/`) + if (searchParams) { + searchParams.forEach((value, key) => { + target.searchParams.set(key, value) + }) + } + + return fetch(target, { + ...init, + headers: { + 'Content-Type': 'application/json', + ...(init?.headers ?? {}), + }, + }) +} + +export function isAgentAlreadyStoppingOrStopped(error: unknown): boolean { + if (!error || typeof error !== 'object') return false + + const maybeErr = error as { + statusCode?: number + body?: { detail?: string; reason?: string } + message?: string + } + + const statusCode = maybeErr.statusCode + const reason = maybeErr.body?.reason?.toLowerCase() + const detail = maybeErr.body?.detail?.toLowerCase() ?? maybeErr.message?.toLowerCase() ?? '' + + if (statusCode === 404) return true + if (reason === 'invalidrequest' && detail.includes('already in the process of shutting down')) { + return true + } + return false +} + +export { ExpiresIn } diff --git a/web-client/src/services/api.ts b/web-client/src/services/api.ts index fbe520d..5d01c8c 100644 --- a/web-client/src/services/api.ts +++ b/web-client/src/services/api.ts @@ -8,8 +8,13 @@ export interface GetConfigResponse { agent_uid: string } -export async function getConfig(): Promise { - const response = await fetch(`${API_BASE_URL}/get_config`, { +export async function getConfig(options?: { channel?: string; uid?: string }): Promise { + const params = new URLSearchParams() + if (options?.channel) params.set('channel', options.channel) + if (options?.uid) params.set('uid', options.uid) + + const query = params.toString() + const response = await fetch(`${API_BASE_URL}/get_config${query ? `?${query}` : ''}`, { method: 'GET', }) @@ -25,19 +30,9 @@ export async function getConfig(): Promise { return result.data } -export async function startAgent(channelName: string, rtcUid: string, userUid: string): Promise { +export async function startAgent(channelName: string, rtcUid: number, userUid: number): Promise { const payload = { channelName, rtcUid, userUid } - // Debug: Log the request payload - console.log('🔍 startAgent Request:', { - url: `${API_BASE_URL}/v2/startAgent`, - method: 'POST', - payload: payload, - curl: `curl -X POST ${window.location.origin}${API_BASE_URL}/v2/startAgent \\ - -H "Content-Type: application/json" \\ - -d '${JSON.stringify(payload, null, 2)}'`, - }) - const response = await fetch(`${API_BASE_URL}/v2/startAgent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -56,13 +51,13 @@ export async function startAgent(channelName: string, rtcUid: string, userUid: s return result.data.agent_id } -export async function stopAgent(channelName: string, agentId: string): Promise { +export async function stopAgent(agentId: string): Promise { if (!agentId) return const response = await fetch(`${API_BASE_URL}/v2/stopAgent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ channelName, agentId }), + body: JSON.stringify({ agentId }), }) if (!response.ok) { diff --git a/web-client/src/stores/app-store.ts b/web-client/src/stores/app-store.ts deleted file mode 100644 index 9e50b62..0000000 --- a/web-client/src/stores/app-store.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { AgentState, type TurnStatus } from 'agora-agent-client-toolkit' -import { create } from 'zustand' - -export type LogLevel = 'info' | 'success' | 'error' | 'warning' - -export interface LogItem { - id: string - message: string - level: LogLevel - timestamp: Date -} - -export interface TranscriptItem { - id: string - type: 'user' | 'agent' - text: string - status: TurnStatus - timestamp: number -} - -interface AppState { - isConnected: boolean - isConnecting: boolean - channelName: string - agentId: string | null - agentState: AgentState - isMicMuted: boolean - transcripts: TranscriptItem[] - logs: LogItem[] - - setChannelName: (name: string) => void - setIsConnecting: (connecting: boolean) => void - setIsConnected: (connected: boolean) => void - setAgentId: (id: string | null) => void - setAgentState: (state: AgentState) => void - setIsMicMuted: (muted: boolean) => void - setTranscripts: (transcripts: TranscriptItem[]) => void - addLog: (message: string, level?: LogLevel) => void - clearLogs: () => void - reset: () => void -} - -const initialState = { - isConnected: false, - isConnecting: false, - channelName: '', - agentId: null, - agentState: AgentState.IDLE, - isMicMuted: false, - transcripts: [], - logs: [], -} - -export const useAppStore = create((set) => ({ - ...initialState, - - setChannelName: (name) => set({ channelName: name }), - setIsConnecting: (connecting) => set({ isConnecting: connecting }), - setIsConnected: (connected) => set({ isConnected: connected }), - setAgentId: (id) => set({ agentId: id }), - setAgentState: (state) => set({ agentState: state }), - setIsMicMuted: (muted) => set({ isMicMuted: muted }), - setTranscripts: (transcripts) => set({ transcripts }), - - addLog: (message, level = 'info') => - set((state) => ({ - logs: [ - ...state.logs, - { - id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, - message, - level, - timestamp: new Date(), - }, - ], - })), - - clearLogs: () => set({ logs: [] }), - reset: () => set(initialState), -})) diff --git a/web-client/tailwind.config.js b/web-client/tailwind.config.js index 0ea137b..8b3e894 100644 --- a/web-client/tailwind.config.js +++ b/web-client/tailwind.config.js @@ -1,6 +1,10 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + content: [ + './app/**/*.{js,ts,jsx,tsx,mdx}', + './src/**/*.{js,ts,jsx,tsx,mdx}', + './node_modules/agora-agent-uikit/dist/**/*.{js,mjs}', + ], theme: { extend: { colors: {