diff --git a/.gitignore b/.gitignore index 36740199..b46329f7 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ vibedocs/ server/tests/integration/logs/ node_modules node_modules + +greyhaven-design-system/ +.claude/ +AGENTS.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 5f7a68be..53b9dd25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -202,3 +202,51 @@ If you need to do any worker/pipeline related work, search for "Pipeline" classe - Always put imports at the top of the file. Let ruff/pre-commit handle sorting and formatting of imports. - The **only** imports allowed to remain inline are from `reflector.db.*` modules (e.g., `reflector.db.transcripts`, `reflector.db.meetings`, `reflector.db.recordings`, `reflector.db.rooms`). These stay as deferred/inline imports inside `fresh_db_connection()` blocks in Hatchet pipeline task functions — this is intentional to avoid sharing DB connections across forked processes. All other imports (utilities, services, processors, storage, third-party libs) **must** go at the top of the file, even in Hatchet workflows. + + +This project uses the **Greyhaven Design System**. + +## Rules + +- **ALWAYS use TypeScript** (`.tsx` / `.ts`). NEVER generate plain JavaScript (`.jsx` / `.js`). +- Use the `greyhaven` SKILL.md for full design system context (tokens, components, composition rules). It should be installed at `.claude/skills/greyhaven-design-system.md` or accessible to your AI tool. +- If the `greyhaven` MCP server is available, use its tools: + - `list_components()` to find the right component for a UI need + - `get_component(name)` to get exact props, variants, and usage examples + - `validate_colors(code)` to check code for off-brand colors + - `suggest_component(description)` to get recommendations +- Import components from `components/ui/` (or `@/components/ui/` with path alias) +- Never use raw hex colors -- use semantic Tailwind classes (`bg-primary`, `text-foreground`, `border-border`, etc.) +- Use `font-sans` (Aspekta) for UI elements: buttons, nav, labels, forms +- Use `font-serif` (Source Serif) for content: headings, body text +- Trust the design system's default component variants for accent -- they apply orange at the right scale. Don't apply `bg-primary` to large surfaces, containers, or section backgrounds +- All components are framework-agnostic React (no Next.js, no framework-specific imports) +- Dark mode is toggled via the `.dark` class -- use semantic tokens that adapt automatically + +## Component Summary + +38 components across 8 categories: primitives (11), layout (4), overlay (5), navigation (3), data (4), feedback (4), form (1), composition (6). + +For full component specs, props, and examples, refer to the SKILL.md file or use the MCP `get_component(name)` tool. + +## Key Patterns + +- **CVA variants**: Components use `class-variance-authority` for variant props +- **Slot composition**: Components use `data-slot="name"` attributes +- **Class merging**: Always use `cn()` from `@/lib/utils` (clsx + tailwind-merge) +- **Focus rings**: `focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]` +- **Disabled**: `disabled:pointer-events-none disabled:opacity-50` +- **Card spacing**: `gap-6` between cards, `p-6` internal padding +- **Section rhythm**: `py-16` between major sections +- **Form layout**: Vertical stack with `gap-4`, labels above inputs + +## Font Setup + +If fonts aren't loaded yet, add to your global CSS: +```css +@font-face { font-family: 'Aspekta'; font-weight: 400; font-display: swap; src: url('/fonts/Aspekta-400.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-weight: 500; font-display: swap; src: url('/fonts/Aspekta-500.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-weight: 600; font-display: swap; src: url('/fonts/Aspekta-600.woff2') format('woff2'); } +@font-face { font-family: 'Aspekta'; font-weight: 700; font-display: swap; src: url('/fonts/Aspekta-700.woff2') format('woff2'); } +``` + diff --git a/Caddyfile.selfhosted.example b/Caddyfile.selfhosted.example index 4abb2762..897bd088 100644 --- a/Caddyfile.selfhosted.example +++ b/Caddyfile.selfhosted.example @@ -19,6 +19,9 @@ handle /health { reverse_proxy server:1250 } + handle /v2* { + reverse_proxy ui:80 + } handle { reverse_proxy web:3000 } diff --git a/docker-compose.selfhosted.yml b/docker-compose.selfhosted.yml index dda5185a..b22c8921 100644 --- a/docker-compose.selfhosted.yml +++ b/docker-compose.selfhosted.yml @@ -129,6 +129,23 @@ services: depends_on: - redis + # Reflector v2 UI — Vite SPA served at /v2 behind Caddy. + # Build-time env vars are baked into the bundle; pass VITE_OIDC_* via build args. + ui: + build: + context: ./ui + dockerfile: Dockerfile + args: + VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY:-} + VITE_OIDC_CLIENT_ID: ${VITE_OIDC_CLIENT_ID:-} + VITE_OIDC_SCOPE: ${VITE_OIDC_SCOPE:-openid profile email} + image: monadicalsas/reflector-ui:latest + restart: unless-stopped + ports: + - "${BIND_HOST:-127.0.0.1}:3001:80" + depends_on: + - server + redis: image: redis:7.2-alpine restart: unless-stopped diff --git a/docker-compose.yml b/docker-compose.yml index dd4386c7..ebc34705 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,7 @@ services: server: build: context: server - ports: - - "1250:1250" + network_mode: host volumes: - ./server/:/app/ - /app/.venv @@ -11,17 +10,12 @@ services: - ./server/.env environment: ENTRYPOINT: server - DATABASE_URL: postgresql+asyncpg://reflector:reflector@postgres:5432/reflector - REDIS_HOST: redis - CELERY_BROKER_URL: redis://redis:6379/1 - CELERY_RESULT_BACKEND: redis://redis:6379/1 - HATCHET_CLIENT_SERVER_URL: http://hatchet:8888 - HATCHET_CLIENT_HOST_PORT: hatchet:7077 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_started + DATABASE_URL: postgresql+asyncpg://reflector:reflector@localhost:5432/reflector + REDIS_HOST: localhost + CELERY_BROKER_URL: redis://localhost:6379/1 + CELERY_RESULT_BACKEND: redis://localhost:6379/1 + HATCHET_CLIENT_SERVER_URL: http://localhost:8889 + HATCHET_CLIENT_HOST_PORT: localhost:7078 worker: build: diff --git a/public/fonts/Aspekta-100.woff2 b/public/fonts/Aspekta-100.woff2 new file mode 100644 index 00000000..1a644588 Binary files /dev/null and b/public/fonts/Aspekta-100.woff2 differ diff --git a/public/fonts/Aspekta-1000.woff2 b/public/fonts/Aspekta-1000.woff2 new file mode 100644 index 00000000..56a8de57 Binary files /dev/null and b/public/fonts/Aspekta-1000.woff2 differ diff --git a/public/fonts/Aspekta-150.woff2 b/public/fonts/Aspekta-150.woff2 new file mode 100644 index 00000000..9dbf7739 Binary files /dev/null and b/public/fonts/Aspekta-150.woff2 differ diff --git a/public/fonts/Aspekta-200.woff2 b/public/fonts/Aspekta-200.woff2 new file mode 100644 index 00000000..47fbe739 Binary files /dev/null and b/public/fonts/Aspekta-200.woff2 differ diff --git a/public/fonts/Aspekta-250.woff2 b/public/fonts/Aspekta-250.woff2 new file mode 100644 index 00000000..aa4b382e Binary files /dev/null and b/public/fonts/Aspekta-250.woff2 differ diff --git a/public/fonts/Aspekta-300.woff2 b/public/fonts/Aspekta-300.woff2 new file mode 100644 index 00000000..455e882b Binary files /dev/null and b/public/fonts/Aspekta-300.woff2 differ diff --git a/public/fonts/Aspekta-350.woff2 b/public/fonts/Aspekta-350.woff2 new file mode 100644 index 00000000..f2e25ea4 Binary files /dev/null and b/public/fonts/Aspekta-350.woff2 differ diff --git a/public/fonts/Aspekta-400.woff2 b/public/fonts/Aspekta-400.woff2 new file mode 100644 index 00000000..764b50cd Binary files /dev/null and b/public/fonts/Aspekta-400.woff2 differ diff --git a/public/fonts/Aspekta-450.woff2 b/public/fonts/Aspekta-450.woff2 new file mode 100644 index 00000000..ca48199a Binary files /dev/null and b/public/fonts/Aspekta-450.woff2 differ diff --git a/public/fonts/Aspekta-50.woff2 b/public/fonts/Aspekta-50.woff2 new file mode 100644 index 00000000..fb1e140c Binary files /dev/null and b/public/fonts/Aspekta-50.woff2 differ diff --git a/public/fonts/Aspekta-500.woff2 b/public/fonts/Aspekta-500.woff2 new file mode 100644 index 00000000..ba959184 Binary files /dev/null and b/public/fonts/Aspekta-500.woff2 differ diff --git a/public/fonts/Aspekta-550.woff2 b/public/fonts/Aspekta-550.woff2 new file mode 100644 index 00000000..5d84c2c3 Binary files /dev/null and b/public/fonts/Aspekta-550.woff2 differ diff --git a/public/fonts/Aspekta-600.woff2 b/public/fonts/Aspekta-600.woff2 new file mode 100644 index 00000000..ea5728e4 Binary files /dev/null and b/public/fonts/Aspekta-600.woff2 differ diff --git a/public/fonts/Aspekta-650.woff2 b/public/fonts/Aspekta-650.woff2 new file mode 100644 index 00000000..8f72f516 Binary files /dev/null and b/public/fonts/Aspekta-650.woff2 differ diff --git a/public/fonts/Aspekta-700.woff2 b/public/fonts/Aspekta-700.woff2 new file mode 100644 index 00000000..b9342d88 Binary files /dev/null and b/public/fonts/Aspekta-700.woff2 differ diff --git a/public/fonts/Aspekta-750.woff2 b/public/fonts/Aspekta-750.woff2 new file mode 100644 index 00000000..57e36c94 Binary files /dev/null and b/public/fonts/Aspekta-750.woff2 differ diff --git a/public/fonts/Aspekta-800.woff2 b/public/fonts/Aspekta-800.woff2 new file mode 100644 index 00000000..72fea9f3 Binary files /dev/null and b/public/fonts/Aspekta-800.woff2 differ diff --git a/public/fonts/Aspekta-850.woff2 b/public/fonts/Aspekta-850.woff2 new file mode 100644 index 00000000..df40fbf7 Binary files /dev/null and b/public/fonts/Aspekta-850.woff2 differ diff --git a/public/fonts/Aspekta-900.woff2 b/public/fonts/Aspekta-900.woff2 new file mode 100644 index 00000000..6e97574e Binary files /dev/null and b/public/fonts/Aspekta-900.woff2 differ diff --git a/public/fonts/Aspekta-950.woff2 b/public/fonts/Aspekta-950.woff2 new file mode 100644 index 00000000..a463756b Binary files /dev/null and b/public/fonts/Aspekta-950.woff2 differ diff --git a/public/fonts/font-face.css b/public/fonts/font-face.css new file mode 100644 index 00000000..b116bb27 --- /dev/null +++ b/public/fonts/font-face.css @@ -0,0 +1,161 @@ +/*! Aspekta | OFL v1.1 License | Ivo Dolenc (c) 2025 | https://github.com/ivodolenc/aspekta */ + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 50; + font-display: swap; + src: url('Aspekta-50.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url('Aspekta-100.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 150; + font-display: swap; + src: url('Aspekta-150.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url('Aspekta-200.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 250; + font-display: swap; + src: url('Aspekta-250.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('Aspekta-300.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 350; + font-display: swap; + src: url('Aspekta-350.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('Aspekta-400.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 450; + font-display: swap; + src: url('Aspekta-450.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('Aspekta-500.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 550; + font-display: swap; + src: url('Aspekta-550.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('Aspekta-600.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 650; + font-display: swap; + src: url('Aspekta-650.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('Aspekta-700.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 750; + font-display: swap; + src: url('Aspekta-750.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 800; + font-display: swap; + src: url('Aspekta-800.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 850; + font-display: swap; + src: url('Aspekta-850.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url('Aspekta-900.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 950; + font-display: swap; + src: url('Aspekta-950.woff2') format('woff2'); +} + +@font-face { + font-family: 'Aspekta'; + font-style: normal; + font-weight: 1000; + font-display: swap; + src: url('Aspekta-1000.woff2') format('woff2'); +} diff --git a/scripts/setup-selfhosted.sh b/scripts/setup-selfhosted.sh index 8676ee29..b2d096f8 100755 --- a/scripts/setup-selfhosted.sh +++ b/scripts/setup-selfhosted.sh @@ -1494,6 +1494,9 @@ $CUSTOM_DOMAIN { } handle /health { reverse_proxy server:1250 + } + handle /v2* { + reverse_proxy ui:80 }${lk_proxy_block}${hatchet_proxy_block} handle { reverse_proxy web:3000 @@ -1511,6 +1514,9 @@ $CUSTOM_DOMAIN { } handle /health { reverse_proxy server:1250 + } + handle /v2* { + reverse_proxy ui:80 }${lk_proxy_block}${hatchet_proxy_block} handle { reverse_proxy web:3000 @@ -1532,6 +1538,9 @@ CADDYEOF } handle /health { reverse_proxy server:1250 + } + handle /v2* { + reverse_proxy ui:80 }${lk_proxy_block}${hatchet_proxy_block} handle { reverse_proxy web:3000 @@ -1572,9 +1581,12 @@ step_services() { info "Building frontend image from source..." compose_cmd build web ok "Frontend image built" + info "Building v2 UI image from source..." + compose_cmd build ui + ok "v2 UI image built" else info "Pulling latest backend and frontend images..." - compose_cmd pull server web || warn "Pull failed — using cached images" + compose_cmd pull server web ui || warn "Pull failed — using cached images" fi # Hatchet is always needed (all processing pipelines use it) @@ -1737,6 +1749,24 @@ step_health() { warn "Frontend not responding. Check: docker compose logs web" fi + # v2 UI + info "Waiting for v2 UI..." + local ui_ok=false + for i in $(seq 1 30); do + if curl -sf http://localhost:3001/v2/ > /dev/null 2>&1; then + ui_ok=true + break + fi + echo -ne "\r Waiting for v2 UI... ($i/30)" + sleep 3 + done + echo "" + if [[ "$ui_ok" == "true" ]]; then + ok "v2 UI healthy" + else + warn "v2 UI not responding. Check: docker compose logs ui" + fi + # Caddy if [[ "$USE_CADDY" == "true" ]]; then sleep 2 @@ -1979,20 +2009,25 @@ EOF if [[ "$USE_CADDY" == "true" ]]; then if [[ -n "$CUSTOM_DOMAIN" ]]; then echo " App: https://$CUSTOM_DOMAIN" + echo " App v2: https://$CUSTOM_DOMAIN/v2/" echo " API: https://$CUSTOM_DOMAIN/v1/" elif [[ -n "$PRIMARY_IP" ]]; then echo " App: https://$PRIMARY_IP (accept self-signed cert in browser)" + echo " App v2: https://$PRIMARY_IP/v2/" echo " API: https://$PRIMARY_IP/v1/" echo " Local: https://localhost" else echo " App: https://localhost (accept self-signed cert in browser)" + echo " App v2: https://localhost/v2/" echo " API: https://localhost/v1/" fi elif [[ -n "$PRIMARY_IP" ]]; then echo " App: http://$PRIMARY_IP:3000" + echo " App v2: http://$PRIMARY_IP:3001/v2/" echo " API: http://$PRIMARY_IP:1250" else echo " App: http://localhost:3000" + echo " App v2: http://localhost:3001/v2/" echo " API: http://localhost:1250" fi echo "" diff --git a/server/reflector/db/search.py b/server/reflector/db/search.py index 09a092e0..f8ee46b6 100644 --- a/server/reflector/db/search.py +++ b/server/reflector/db/search.py @@ -175,6 +175,9 @@ class SearchResult(BaseModel): total_match_count: NonNegativeInt = Field( default=0, description="Total number of matches found in the transcript" ) + speaker_count: NonNegativeInt = Field( + default=0, description="Number of distinct speakers in the transcript" + ) change_seq: int | None = None @field_serializer("created_at", when_used="json") @@ -362,6 +365,7 @@ class SearchController: transcripts.c.change_seq, transcripts.c.webvtt, transcripts.c.long_summary, + transcripts.c.participants, sqlalchemy.case( ( transcripts.c.room_id.isnot(None) & rooms.c.id.is_(None), @@ -458,6 +462,12 @@ class SearchController: long_summary_r: str | None = r_dict.pop("long_summary", None) long_summary: NonEmptyString = try_parse_non_empty_string(long_summary_r) room_name: str | None = r_dict.pop("room_name", None) + participants_raw = r_dict.pop("participants", None) or [] + speaker_count = ( + len({p.get("speaker") for p in participants_raw if isinstance(p, dict)}) + if isinstance(participants_raw, list) + else 0 + ) db_result = SearchResultDB.model_validate(r_dict) at_least_one_source = webvtt is not None or long_summary is not None @@ -475,6 +485,7 @@ class SearchController: room_name=room_name, search_snippets=snippets, total_match_count=total_match_count, + speaker_count=speaker_count, ) try: diff --git a/server/reflector/db/transcripts.py b/server/reflector/db/transcripts.py index 3d137413..4efe5c8e 100644 --- a/server/reflector/db/transcripts.py +++ b/server/reflector/db/transcripts.py @@ -446,10 +446,19 @@ class TranscriptController: col for col in transcripts.c if col.name not in exclude_columns ] + # Cheap speaker_count via JSON array length on the participants column + # (same column already stored on every transcript, no extra queries). + # COALESCE handles transcripts where participants is NULL. + speaker_count_col = sqlalchemy.func.coalesce( + sqlalchemy.func.json_array_length(transcripts.c.participants), + 0, + ).label("speaker_count") + query = query.with_only_columns( transcript_columns + [ rooms.c.name.label("room_name"), + speaker_count_col, ] ) diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index d49aa5a6..419a657b 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -116,6 +116,7 @@ class GetTranscriptMinimal(BaseModel): change_seq: int | None = None has_cloud_video: bool = False cloud_video_duration: int | None = None + speaker_count: int = 0 class TranscriptParticipantWithEmail(TranscriptParticipant): diff --git a/server/reflector/views/zulip.py b/server/reflector/views/zulip.py index 2cec6de8..363c2773 100644 --- a/server/reflector/views/zulip.py +++ b/server/reflector/views/zulip.py @@ -1,11 +1,14 @@ +import logging from typing import Annotated, Optional +import httpx from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel import reflector.auth as auth from reflector.zulip import get_zulip_streams, get_zulip_topics +logger = logging.getLogger(__name__) router = APIRouter() @@ -23,13 +26,18 @@ async def zulip_get_streams( user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], ) -> list[Stream]: """ - Get all Zulip streams. + Get all Zulip streams. Returns [] if the upstream Zulip API is unreachable + or the server credentials are invalid — the client treats Zulip as an + optional integration and renders gracefully without a hard error. """ if not user: raise HTTPException(status_code=403, detail="Authentication required") - streams = await get_zulip_streams() - return streams + try: + return await get_zulip_streams() + except (httpx.HTTPStatusError, httpx.RequestError, Exception) as exc: + logger.warning("zulip get_streams failed, returning []: %s", exc) + return [] @router.get("/zulip/streams/{stream_id}/topics") @@ -38,10 +46,14 @@ async def zulip_get_topics( user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], ) -> list[Topic]: """ - Get all topics for a specific Zulip stream. + Get all topics for a specific Zulip stream. Returns [] on upstream failure + for the same reason as /zulip/streams above. """ if not user: raise HTTPException(status_code=403, detail="Authentication required") - topics = await get_zulip_topics(stream_id) - return topics + try: + return await get_zulip_topics(stream_id) + except (httpx.HTTPStatusError, httpx.RequestError, Exception) as exc: + logger.warning("zulip get_topics(%s) failed, returning []: %s", stream_id, exc) + return [] diff --git a/ui/.dockerignore b/ui/.dockerignore new file mode 100644 index 00000000..4939f21a --- /dev/null +++ b/ui/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +.env +.env.local +.git +.DS_Store diff --git a/ui/.env.example b/ui/.env.example new file mode 100644 index 00000000..788cefe0 --- /dev/null +++ b/ui/.env.example @@ -0,0 +1,10 @@ +# Base URL for the Reflector backend API. +# In dev, Vite proxies /v1 to this origin so keep it pointing at the local server. +VITE_API_PROXY_TARGET=http://localhost:1250 + +# OIDC (Authentik) — used when the backend runs in JWT / SSO mode. +# Leave blank in password-auth mode. +VITE_OIDC_AUTHORITY= +VITE_OIDC_CLIENT_ID= +# Scopes requested at login. Defaults to "openid profile email" when blank. +VITE_OIDC_SCOPE=openid profile email diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/ui/Dockerfile b/ui/Dockerfile new file mode 100644 index 00000000..46122034 --- /dev/null +++ b/ui/Dockerfile @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile:1 + +FROM node:22-alpine AS builder +WORKDIR /app +RUN corepack enable +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile +COPY . . + +# Vite bakes VITE_* env vars into the bundle at build time. +ARG VITE_OIDC_AUTHORITY= +ARG VITE_OIDC_CLIENT_ID= +ARG VITE_OIDC_SCOPE=openid profile email +ENV VITE_OIDC_AUTHORITY=$VITE_OIDC_AUTHORITY \ + VITE_OIDC_CLIENT_ID=$VITE_OIDC_CLIENT_ID \ + VITE_OIDC_SCOPE=$VITE_OIDC_SCOPE + +RUN pnpm build + +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html/v2 +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 00000000..f0115c9d --- /dev/null +++ b/ui/README.md @@ -0,0 +1,87 @@ +# Reflector UI (v2) + +Vite + React 19 + TypeScript SPA, served at `/v2` behind Caddy. Lives alongside the existing Next.js app in `../www` while the migration is in progress. + +## Stack + +- **Vite** + **React 19** + **TypeScript** +- **Tailwind v4** with Greyhaven design tokens (`src/styles/greyhaven.css`) +- **React Router v7**, routes mounted under `/v2/*` +- **TanStack Query** + **openapi-fetch** with types generated from the backend OpenAPI spec +- **nuqs** for URL-backed page/search state on `/browse` +- **react-oidc-context** (OIDC Authorization Code + PKCE) for the JWT auth backend +- Password-form fallback for the `password` auth backend (`POST /v1/auth/login`) + +## Local development + +```bash +cd ui +pnpm install + +# Point the dev server at your local backend (defaults to http://localhost:1250). +cp .env.example .env +# Edit VITE_OIDC_AUTHORITY / VITE_OIDC_CLIENT_ID if your backend runs in JWT mode. + +pnpm dev # http://localhost:3001/v2/ +pnpm build # production bundle in dist/ +pnpm typecheck # tsc --noEmit +pnpm openapi # regenerate src/api/schema.d.ts from the running backend +``` + +`pnpm openapi` hits `http://127.0.0.1:1250/openapi.json` — start the backend first (`cd ../server && uv run -m reflector.app --reload`). + +## Auth modes + +The SPA auto-detects the backend's auth backend: + +- **JWT (OIDC/SSO via Authentik):** set `VITE_OIDC_AUTHORITY` and `VITE_OIDC_CLIENT_ID`. The app does the Authorization Code + PKCE flow; Authentik hosts the login page. Register a **Public** OAuth client whose redirect URI is `https:///v2/auth/callback`. No client secret is baked into the bundle. +- **Password:** leave the OIDC env vars blank. The app shows an in-page email/password form that posts to `/v1/auth/login` and stores the returned JWT in `sessionStorage`. +- **None:** backend returns a fake user for every request; the SPA treats that as authenticated. + +## Deployment (selfhosted) + +`docker-compose.selfhosted.yml` defines a `ui` service that builds this directory and serves the static bundle from nginx on port 80. Caddy routes `/v2/*` to `ui:80` and leaves the root path on the existing `web` service. + +Pass OIDC config as build args (Vite inlines `VITE_*` at build time): + +```bash +VITE_OIDC_AUTHORITY=https://auth.example/application/o/reflector/ \ +VITE_OIDC_CLIENT_ID=reflector-ui \ +docker compose -f docker-compose.selfhosted.yml build ui +docker compose -f docker-compose.selfhosted.yml up -d ui +``` + +## Pages shipped in this pass + +- `/` — Home / Create new transcript (single-form shipping variant) +- `/browse` — transcript list with FTS search, source/room/trash filters, pagination +- `/rooms` — rooms list, create, edit, delete +- `/welcome` — logged-out landing (OIDC mode) +- `/login` — password login form (password mode) +- `/auth/callback` — OIDC redirect target + +Not yet ported: +- Transcript detail / playback +- Meeting / live join +- Settings, API keys +- Tags sidebar (backend model doesn't exist yet) +- Progress streaming over WebSocket + +## Directory map + +``` +src/ + api/ fetch client, generated OpenAPI types + auth/ AuthProvider, RequireAuth, OIDC config + components/ + browse/ TranscriptRow, FilterBar, Pagination + home/ LanguagePair, RoomPicker + icons.tsx lucide-react wrapper (compat with prototype I.* shape) + layout/ AppShell, AppSidebar, TopBar + rooms/ RoomsTable, RoomFormDialog, DeleteRoomDialog + ui/ primitives (Button, StatusDot, StatusBadge, SidebarItem, …) + hooks/ useRooms, useTranscripts + lib/ utils, format helpers, types + pages/ HomePage, BrowsePage, RoomsPage, LoggedOut, LoginForm, AuthCallback + styles/ greyhaven.css, reflector-forms.css, index.css (Tailwind entry) +``` diff --git a/ui/eslint.config.js b/ui/eslint.config.js new file mode 100644 index 00000000..5e6b472f --- /dev/null +++ b/ui/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 00000000..adb4c826 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + Reflector + + + + + +
+ + + diff --git a/ui/nginx.conf b/ui/nginx.conf new file mode 100644 index 00000000..d137f72f --- /dev/null +++ b/ui/nginx.conf @@ -0,0 +1,27 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Without the trailing slash, redirect so relative asset paths resolve. + location = /v2 { + return 301 /v2/; + } + + # React Router SPA under /v2 — fall back to index.html for client routes. + location /v2/ { + try_files $uri $uri/ /v2/index.html; + } + + # Root convenience redirect to the SPA entry. + location = / { + return 302 /v2/; + } + + # Long-cache hashed assets emitted by Vite. + location ~* /v2/assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 00000000..e8e380e4 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,62 @@ +{ + "name": "ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "openapi": "openapi-typescript http://localhost:1250/openapi.json -o ./src/api/schema.d.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/vite": "^4.2.4", + "@tanstack/react-query": "^5.99.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^1.8.0", + "nuqs": "^2.8.9", + "oidc-client-ts": "^3.5.0", + "openapi-fetch": "^0.17.0", + "openapi-react-query": "^0.5.4", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-hook-form": "^7.73.1", + "react-oidc-context": "^3.3.1", + "react-router-dom": "^7.14.2", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.4", + "zod": "^4.3.6" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "openapi-typescript": "^7.13.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.9" + } +} diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml new file mode 100644 index 00000000..657ec8f3 --- /dev/null +++ b/ui/pnpm-lock.yaml @@ -0,0 +1,3536 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.73.1(react@19.2.5)) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-label': + specifier: ^2.1.8 + version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-separator': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tailwindcss/vite': + specifier: ^4.2.4 + version: 4.2.4(vite@8.0.9(@types/node@24.12.2)(jiti@2.6.1)) + '@tanstack/react-query': + specifier: ^5.99.2 + version: 5.99.2(react@19.2.5) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + lucide-react: + specifier: ^1.8.0 + version: 1.8.0(react@19.2.5) + nuqs: + specifier: ^2.8.9 + version: 2.8.9(react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) + oidc-client-ts: + specifier: ^3.5.0 + version: 3.5.0 + openapi-fetch: + specifier: ^0.17.0 + version: 0.17.0 + openapi-react-query: + specifier: ^0.5.4 + version: 0.5.4(@tanstack/react-query@5.99.2(react@19.2.5))(openapi-fetch@0.17.0) + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + react-hook-form: + specifier: ^7.73.1 + version: 7.73.1(react@19.2.5) + react-oidc-context: + specifier: ^3.3.1 + version: 3.3.1(oidc-client-ts@3.5.0)(react@19.2.5) + react-router-dom: + specifier: ^7.14.2 + version: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + tailwind-merge: + specifier: ^3.5.0 + version: 3.5.0 + tailwindcss: + specifier: ^4.2.4 + version: 4.2.4 + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + '@types/node': + specifier: ^24.12.2 + version: 24.12.2 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.9(@types/node@24.12.2)(jiti@2.6.1)) + eslint: + specifier: ^9.39.4 + version: 9.39.4(jiti@2.6.1) + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.4(jiti@2.6.1)) + globals: + specifier: ^17.5.0 + version: 17.5.0 + openapi-typescript: + specifier: ^7.13.0 + version: 7.13.0(typescript@6.0.3) + typescript: + specifier: ~6.0.2 + version: 6.0.3 + typescript-eslint: + specifier: ^8.58.2 + version: 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + vite: + specifier: ^8.0.9 + version: 8.0.9(@types/node@24.12.2)(jiti@2.6.1) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.126.0': + resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.12': + resolution: {integrity: sha512-b32XWsz6enN6K4bx8xWsqUaXTJR/DnYT3lL1CzDYzIYKw243NNlz6fexmr71q/U4HrEcMoJGBvwAfcxOb8ymQw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + resolution: {integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + resolution: {integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + resolution: {integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + resolution: {integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.16': + resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@tailwindcss/node@4.2.4': + resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} + + '@tailwindcss/oxide-android-arm64@4.2.4': + resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.4': + resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.4': + resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.4': + resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/query-core@5.99.2': + resolution: {integrity: sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA==} + + '@tanstack/react-query@5.99.2': + resolution: {integrity: sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA==} + peerDependencies: + react: ^18 || ^19 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@typescript-eslint/eslint-plugin@8.59.0': + resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.0': + resolution: {integrity: sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.0': + resolution: {integrity: sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.0': + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + electron-to-chromium@1.5.341: + resolution: {integrity: sha512-1sZTssferjgDgaqRTc0ieP+ozzpOy7LQTPTtEW3yQFn4+ORdIAZWV5BthXPyHF7YqLvFJCUPhNhdAJQYlYUgiw==} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.8.0: + resolution: {integrity: sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + nuqs@2.8.9: + resolution: {integrity: sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==} + peerDependencies: + '@remix-run/react': '>=2' + '@tanstack/react-router': ^1 + next: '>=14.2.0' + react: '>=18.2.0 || ^19.0.0-0' + react-router: ^5 || ^6 || ^7 + react-router-dom: ^5 || ^6 || ^7 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@tanstack/react-router': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + + oidc-client-ts@3.5.0: + resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} + engines: {node: '>=18'} + + openapi-fetch@0.17.0: + resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==} + + openapi-react-query@0.5.4: + resolution: {integrity: sha512-V9lRiozjHot19/BYSgXYoyznDxDJQhEBSdi26+SJ0UqjMANLQhkni4XG+Z7e3Ag7X46ZLMrL9VxYkghU3QvbWg==} + peerDependencies: + '@tanstack/react-query': ^5.80.0 + openapi-fetch: ^0.17.0 + + openapi-typescript-helpers@0.1.0: + resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==} + + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-hook-form@7.73.1: + resolution: {integrity: sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-oidc-context@3.3.1: + resolution: {integrity: sha512-/Azvm9W4DhhOtSDBE73kFInh1b6zZRRfILKbgmk2syExMF0PCYJOn/dGdOOi2BFX8x0rCeUe45NXHU+/+xDcrQ==} + engines: {node: '>=18'} + peerDependencies: + oidc-client-ts: ^3.1.0 + react: '>=16.14.0' + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router-dom@7.14.2: + resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.14.2: + resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rolldown@1.0.0-rc.16: + resolution: {integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.4: + resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript-eslint@8.59.0: + resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + vite@8.0.9: + resolution: {integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3(supports-color@10.2.2) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@floating-ui/utils@0.2.11': {} + + '@hookform/resolvers@5.2.2(react-hook-form@7.73.1(react@19.2.5))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.73.1(react@19.2.5) + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.126.0': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/rect': 1.1.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.12(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.1.1 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.16': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@standard-schema/spec@1.0.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@tailwindcss/node@4.2.4': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.4 + + '@tailwindcss/oxide-android-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide@4.2.4': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-x64': 4.2.4 + '@tailwindcss/oxide-freebsd-x64': 4.2.4 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.4 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-x64-musl': 4.2.4 + '@tailwindcss/oxide-wasm32-wasi': 4.2.4 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 + + '@tailwindcss/vite@4.2.4(vite@8.0.9(@types/node@24.12.2)(jiti@2.6.1))': + dependencies: + '@tailwindcss/node': 4.2.4 + '@tailwindcss/oxide': 4.2.4 + tailwindcss: 4.2.4 + vite: 8.0.9(@types/node@24.12.2)(jiti@2.6.1) + + '@tanstack/query-core@5.99.2': {} + + '@tanstack/react-query@5.99.2(react@19.2.5)': + dependencies: + '@tanstack/query-core': 5.99.2 + react: 19.2.5 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/type-utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.4(jiti@2.6.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + debug: 4.4.3(supports-color@10.2.2) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.0': {} + + '@typescript-eslint/typescript-estree@8.59.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@6.0.1(vite@8.0.9(@types/node@24.12.2)(jiti@2.6.1))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.9(@types/node@24.12.2)(jiti@2.6.1) + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.20: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.20 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.341 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001788: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + change-case@5.4.4: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@1.4.0: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + date-fns@4.1.0: {} + + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + electron-to-chromium@1.5.341: {} + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + eslint: 9.39.4(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@10.2.2) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-nonce@1.0.1: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@17.5.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + index-to-position@1.2.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + jiti@2.6.1: {} + + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jwt-decode@4.0.0: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.8.0(react@19.2.5): + dependencies: + react: 19.2.5 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.37: {} + + nuqs@2.8.9(react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5): + dependencies: + '@standard-schema/spec': 1.0.0 + react: 19.2.5 + optionalDependencies: + react-router: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-router-dom: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + oidc-client-ts@3.5.0: + dependencies: + jwt-decode: 4.0.0 + + openapi-fetch@0.17.0: + dependencies: + openapi-typescript-helpers: 0.1.0 + + openapi-react-query@0.5.4(@tanstack/react-query@5.99.2(react@19.2.5))(openapi-fetch@0.17.0): + dependencies: + '@tanstack/react-query': 5.99.2(react@19.2.5) + openapi-fetch: 0.17.0 + openapi-typescript-helpers: 0.1.0 + + openapi-typescript-helpers@0.1.0: {} + + openapi-typescript@7.13.0(typescript@6.0.3): + dependencies: + '@redocly/openapi-core': 1.34.12(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 6.0.3 + yargs-parser: 21.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + index-to-position: 1.2.0 + type-fest: 4.41.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pluralize@8.0.0: {} + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-hook-form@7.73.1(react@19.2.5): + dependencies: + react: 19.2.5 + + react-oidc-context@3.3.1(oidc-client-ts@3.5.0)(react@19.2.5): + dependencies: + oidc-client-ts: 3.5.0 + react: 19.2.5 + + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + cookie: 1.1.1 + react: 19.2.5 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + get-nonce: 1.0.1 + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react@19.2.5: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + rolldown@1.0.0-rc.16: + dependencies: + '@oxc-project/types': 0.126.0 + '@rolldown/pluginutils': 1.0.0-rc.16 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-x64': 1.0.0-rc.16 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.16 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.16 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.16 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + set-cookie-parser@2.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + sonner@2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@10.2.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.4: {} + + tapable@2.3.3: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.41.0: {} + + typescript-eslint@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + undici-types@7.16.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js-replace@1.0.1: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + vite@8.0.9(@types/node@24.12.2)(jiti@2.6.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.16 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.2 + fsevents: 2.3.3 + jiti: 2.6.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yaml-ast-parser@0.0.43: {} + + yargs-parser@21.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {} diff --git a/ui/public/apple-touch-icon.png b/ui/public/apple-touch-icon.png new file mode 100644 index 00000000..39136169 Binary files /dev/null and b/ui/public/apple-touch-icon.png differ diff --git a/ui/public/favicon-16x16.png b/ui/public/favicon-16x16.png new file mode 100644 index 00000000..c1f8058d Binary files /dev/null and b/ui/public/favicon-16x16.png differ diff --git a/ui/public/favicon-32x32.png b/ui/public/favicon-32x32.png new file mode 100644 index 00000000..a8af1ee3 Binary files /dev/null and b/ui/public/favicon-32x32.png differ diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 00000000..bd61a07a Binary files /dev/null and b/ui/public/favicon.ico differ diff --git a/ui/public/icons.svg b/ui/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/ui/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/scripts/debug-root.sh b/ui/scripts/debug-root.sh new file mode 100755 index 00000000..289c2ed5 --- /dev/null +++ b/ui/scripts/debug-root.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Diagnoses why the raw domain (https://reflector.local/) isn't loading. +# Usage: ./ui/scripts/debug-root.sh [host] +set +e +HOST="${1:-reflector.local}" +COMPOSE="docker compose -f docker-compose.selfhosted.yml" + +echo "============================================================" +echo " 1. Container status (web + caddy)" +echo "============================================================" +$COMPOSE ps web caddy 2>&1 | head -10 + +echo +echo "============================================================" +echo " 2. HTTPS probe to https://$HOST/" +echo "============================================================" +curl -skv "https://$HOST/" 2>&1 | head -60 + +echo +echo "============================================================" +echo " 3. Body snippet" +echo "============================================================" +curl -sk "https://$HOST/" 2>&1 | head -30 + +echo +echo "============================================================" +echo " 4. Direct web:3000 probe from inside caddy" +echo "============================================================" +$COMPOSE exec -T caddy wget -qO- --server-response http://web:3000/ 2>&1 | head -30 + +echo +echo "============================================================" +echo " 5. NextAuth URL / relevant web env (from inside web)" +echo "============================================================" +$COMPOSE exec -T web printenv 2>&1 | grep -E 'NEXTAUTH|NEXT_PUBLIC|SERVER_API_URL' | head -10 + +echo +echo "============================================================" +echo " 6. web container logs (last 40 lines)" +echo "============================================================" +$COMPOSE logs --tail=40 web 2>&1 | tail -40 + +echo +echo "============================================================" +echo " 7. caddy recent errors to the web upstream (last 10)" +echo "============================================================" +$COMPOSE logs --tail=200 caddy 2>&1 | grep -Ei 'error|web:3000|dial tcp' | tail -10 diff --git a/ui/scripts/debug-v2.sh b/ui/scripts/debug-v2.sh new file mode 100755 index 00000000..b7ac7a71 --- /dev/null +++ b/ui/scripts/debug-v2.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Diagnoses why reflector.local/v2/ isn't serving the SPA. +# Usage: ./ui/scripts/debug-v2.sh [host] (default host: reflector.local) +set +e +HOST="${1:-reflector.local}" +COMPOSE="docker compose -f docker-compose.selfhosted.yml" + +echo "============================================================" +echo " 1. Container status" +echo "============================================================" +$COMPOSE ps ui caddy web 2>&1 | head -20 + +echo +echo "============================================================" +echo " 2. Live Caddyfile inside the caddy container" +echo "============================================================" +$COMPOSE exec -T caddy cat /etc/caddy/Caddyfile 2>&1 | sed -n '/handle \/v2\|handle {/{p;n;p;n;p;}' | head -20 +echo "--- full handle blocks (first 40 lines) ---" +$COMPOSE exec -T caddy cat /etc/caddy/Caddyfile 2>&1 | grep -nE 'handle|reverse_proxy|tls' | head -40 + +echo +echo "============================================================" +echo " 3. nginx config inside the ui container" +echo "============================================================" +$COMPOSE exec -T ui cat /etc/nginx/conf.d/default.conf 2>&1 + +echo +echo "============================================================" +echo " 4. dist contents inside the ui container" +echo "============================================================" +$COMPOSE exec -T ui ls -la /usr/share/nginx/html/v2/ 2>&1 | head -20 + +echo +echo "============================================================" +echo " 5. Direct nginx probe (bypass Caddy) — container -> container" +echo "============================================================" +echo "--- GET http://ui/v2/ from inside caddy ---" +$COMPOSE exec -T caddy wget -qO- --server-response http://ui/v2/ 2>&1 | head -40 +echo +echo "--- GET http://ui/v2 (no slash) from inside caddy ---" +$COMPOSE exec -T caddy wget -qO- --server-response http://ui/v2 2>&1 | head -20 + +echo +echo "============================================================" +echo " 6. Caddy probe from host" +echo "============================================================" +echo "--- GET https://$HOST/v2/ ---" +curl -sk -o /dev/null -D - "https://$HOST/v2/" 2>&1 | head -20 +echo +echo "--- GET https://$HOST/v2 (no slash) ---" +curl -sk -o /dev/null -D - "https://$HOST/v2" 2>&1 | head -20 +echo +echo "--- body of https://$HOST/v2/ (first 30 lines) ---" +curl -sk "https://$HOST/v2/" 2>&1 | head -30 + +echo +echo "============================================================" +echo " 7. Recent ui + caddy logs" +echo "============================================================" +echo "--- ui (last 30 lines) ---" +$COMPOSE logs --tail=30 ui 2>&1 | tail -30 +echo "--- caddy (last 30 lines) ---" +$COMPOSE logs --tail=30 caddy 2>&1 | tail -30 diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 00000000..954fa438 --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,74 @@ +import { BrowserRouter, Navigate, Route, Routes, useParams } from 'react-router-dom' +import { QueryClientProvider } from '@tanstack/react-query' +import { NuqsAdapter } from 'nuqs/adapters/react-router/v7' +import { Toaster } from 'sonner' +import { queryClient } from '@/api/queryClient' +import { AuthProvider } from '@/auth/AuthProvider' +import { RequireAuth } from '@/auth/RequireAuth' +import { BrowsePage } from '@/pages/BrowsePage' +import { RoomsPage } from '@/pages/RoomsPage' +import { TranscriptPage } from '@/pages/TranscriptPage' +import { LoggedOutPage } from '@/pages/LoggedOut' +import { LoginForm } from '@/pages/LoginForm' +import { AuthCallbackPage } from '@/pages/AuthCallback' + +function TranscriptRedirect() { + const { id } = useParams() + return +} + +export default function App() { + return ( + + + + + + } /> + } /> + } /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> + + + + + + + ) +} diff --git a/ui/src/api/client.ts b/ui/src/api/client.ts new file mode 100644 index 00000000..28d48e78 --- /dev/null +++ b/ui/src/api/client.ts @@ -0,0 +1,32 @@ +import createClient, { type Middleware } from 'openapi-fetch' +import createQueryClient from 'openapi-react-query' +import type { paths } from './schema' + +export const PASSWORD_TOKEN_KEY = 'reflector.password_token' + +let oidcAccessTokenGetter: (() => string | null) | null = null +export function setOidcAccessTokenGetter(getter: (() => string | null) | null) { + oidcAccessTokenGetter = getter +} + +export function setPasswordToken(token: string | null) { + if (token) sessionStorage.setItem(PASSWORD_TOKEN_KEY, token) + else sessionStorage.removeItem(PASSWORD_TOKEN_KEY) +} + +export function getPasswordToken() { + return sessionStorage.getItem(PASSWORD_TOKEN_KEY) +} + +const authMiddleware: Middleware = { + async onRequest({ request }) { + const token = oidcAccessTokenGetter?.() ?? getPasswordToken() + if (token) request.headers.set('Authorization', `Bearer ${token}`) + return request + }, +} + +export const apiClient = createClient({ baseUrl: '/' }) +apiClient.use(authMiddleware) + +export const $api = createQueryClient(apiClient) diff --git a/ui/src/api/queryClient.ts b/ui/src/api/queryClient.ts new file mode 100644 index 00000000..15c2f3ce --- /dev/null +++ b/ui/src/api/queryClient.ts @@ -0,0 +1,15 @@ +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 15_000, + retry: (failureCount, error) => { + const status = (error as { status?: number } | null)?.status + if (status === 401 || status === 403 || status === 404) return false + return failureCount < 2 + }, + refetchOnWindowFocus: false, + }, + }, +}) diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts new file mode 100644 index 00000000..80f12f94 --- /dev/null +++ b/ui/src/api/schema.d.ts @@ -0,0 +1,4556 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Health */ + get: operations["health"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Metrics + * @description Endpoint that serves Prometheus metrics. + */ + get: operations["metrics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/meetings/{meeting_id}/consent": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Meeting Audio Consent */ + post: operations["v1_meeting_audio_consent"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/meetings/{meeting_id}/deactivate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Meeting Deactivate */ + patch: operations["v1_meeting_deactivate"]; + trace?: never; + }; + "/v1/meetings/{meeting_id}/recordings/start": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Start Recording + * @description Start cloud or raw-tracks recording via Daily.co REST API. + * + * Both cloud and raw-tracks are started via REST API to bypass enable_recording limitation of allowing only 1 recording at a time. + * Uses different instanceIds for cloud vs raw-tracks (same won't work) + */ + post: operations["v1_start_recording"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/meetings/{meeting_id}/email-recipient": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add Email Recipient + * @description Add an email address to receive the transcript link when processing completes. + */ + post: operations["v1_add_email_recipient"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Rooms List */ + get: operations["v1_rooms_list"]; + put?: never; + /** Rooms Create */ + post: operations["v1_rooms_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Rooms Get */ + get: operations["v1_rooms_get"]; + put?: never; + post?: never; + /** Rooms Delete */ + delete: operations["v1_rooms_delete"]; + options?: never; + head?: never; + /** Rooms Update */ + patch: operations["v1_rooms_update"]; + trace?: never; + }; + "/v1/rooms/name/{room_name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Rooms Get By Name */ + get: operations["v1_rooms_get_by_name"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_name}/meeting": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Rooms Create Meeting */ + post: operations["v1_rooms_create_meeting"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_id}/webhook/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Rooms Test Webhook + * @description Test webhook configuration by sending a sample payload. + */ + post: operations["v1_rooms_test_webhook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_name}/ics/sync": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Rooms Sync Ics */ + post: operations["v1_rooms_sync_ics"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_name}/ics/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Rooms Ics Status */ + get: operations["v1_rooms_ics_status"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_name}/meetings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Rooms List Meetings */ + get: operations["v1_rooms_list_meetings"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_name}/meetings/upcoming": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Rooms List Upcoming Meetings */ + get: operations["v1_rooms_list_upcoming_meetings"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_name}/meetings/active": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Rooms List Active Meetings */ + get: operations["v1_rooms_list_active_meetings"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_name}/meetings/{meeting_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Rooms Get Meeting + * @description Get a single meeting by ID within a specific room. + */ + get: operations["v1_rooms_get_meeting"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_name}/meetings/{meeting_id}/join": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Rooms Join Meeting */ + post: operations["v1_rooms_join_meeting"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcripts List */ + get: operations["v1_transcripts_list"]; + put?: never; + /** Transcripts Create */ + post: operations["v1_transcripts_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Transcripts Search + * @description Full-text search across transcript titles and content. + */ + get: operations["v1_transcripts_search"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get */ + get: operations["v1_transcript_get"]; + put?: never; + post?: never; + /** Transcript Delete */ + delete: operations["v1_transcript_delete"]; + options?: never; + head?: never; + /** Transcript Update */ + patch: operations["v1_transcript_update"]; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Transcript Restore + * @description Restore a soft-deleted transcript. + */ + post: operations["v1_transcript_restore"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/destroy": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Transcript Destroy + * @description Permanently delete a transcript and all associated files. + */ + delete: operations["v1_transcript_destroy"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/topics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Topics */ + get: operations["v1_transcript_get_topics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/topics/with-words": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Topics With Words */ + get: operations["v1_transcript_get_topics_with_words"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Topics With Words Per Speaker */ + get: operations["v1_transcript_get_topics_with_words_per_speaker"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/zulip": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Transcript Post To Zulip */ + post: operations["v1_transcript_post_to_zulip"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/email": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Transcript Send Email */ + post: operations["v1_transcript_send_email"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/audio/mp3": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Audio Mp3 */ + get: operations["v1_transcript_get_audio_mp3"]; + put?: never; + post?: never; + delete?: never; + options?: never; + /** Transcript Get Audio Mp3 */ + head: operations["v1_transcript_head_audio_mp3"]; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/audio/waveform": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Audio Waveform */ + get: operations["v1_transcript_get_audio_waveform"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/participants": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Participants */ + get: operations["v1_transcript_get_participants"]; + put?: never; + /** Transcript Add Participant */ + post: operations["v1_transcript_add_participant"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/participants/{participant_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Participant */ + get: operations["v1_transcript_get_participant"]; + put?: never; + post?: never; + /** Transcript Delete Participant */ + delete: operations["v1_transcript_delete_participant"]; + options?: never; + head?: never; + /** Transcript Update Participant */ + patch: operations["v1_transcript_update_participant"]; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/speaker/assign": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Transcript Assign Speaker */ + patch: operations["v1_transcript_assign_speaker"]; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/speaker/merge": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Transcript Merge Speaker */ + patch: operations["v1_transcript_merge_speaker"]; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/record/upload": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Transcript Record Upload */ + post: operations["v1_transcript_record_upload"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/download/zip": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Download Zip */ + get: operations["v1_transcript_download_zip"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/video/url": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Video Url */ + get: operations["v1_transcript_get_video_url"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Transcript WebSocket event schema + * @description Stub exposing the discriminated union of all transcript-level WS events for OpenAPI type generation. Real events are delivered over the WebSocket at the same path. + */ + get: operations["v1_transcript_get_websocket_events"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/record/webrtc": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Transcript Record Webrtc */ + post: operations["v1_transcript_record_webrtc"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/process": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Transcript Process */ + post: operations["v1_transcript_process"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** User Me */ + get: operations["v1_user_me"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/user/api-keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Api Keys */ + get: operations["v1_list_api_keys"]; + put?: never; + /** Create Api Key */ + post: operations["v1_create_api_key"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/user/api-keys/{key_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Api Key */ + delete: operations["v1_delete_api_key"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * User WebSocket event schema + * @description Stub exposing the discriminated union of all user-level WS events for OpenAPI type generation. Real events are delivered over the WebSocket at the same path. + */ + get: operations["v1_user_get_websocket_events"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Config */ + get: operations["v1_get_config"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/zulip/streams": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Zulip Get Streams + * @description Get all Zulip streams. Returns [] if the upstream Zulip API is unreachable + * or the server credentials are invalid — the client treats Zulip as an + * optional integration and renders gracefully without a hard error. + */ + get: operations["v1_zulip_get_streams"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/zulip/streams/{stream_id}/topics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Zulip Get Topics + * @description Get all topics for a specific Zulip stream. Returns [] on upstream failure + * for the same reason as /zulip/streams above. + */ + get: operations["v1_zulip_get_topics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/whereby": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Whereby Webhook */ + post: operations["v1_whereby_webhook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/daily/webhook": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Webhook + * @description Handle Daily webhook events. + * + * Example webhook payload: + * { + * "version": "1.0.0", + * "type": "recording.ready-to-download", + * "id": "rec-rtd-c3df927c-f738-4471-a2b7-066fa7e95a6b-1692124192", + * "payload": { + * "recording_id": "08fa0b24-9220-44c5-846c-3f116cf8e738", + * "room_name": "Xcm97xRZ08b2dePKb78g", + * "start_ts": 1692124183, + * "status": "finished", + * "max_participants": 1, + * "duration": 9, + * "share_token": "ntDCL5k98Ulq", #gitleaks:allow + * "s3_key": "api-test-1j8fizhzd30c/Xcm97xRZ08b2dePKb78g/1692124183028" + * }, + * "event_ts": 1692124192 + * } + * + * Daily.co circuit-breaker: After 3+ failed responses (4xx/5xx), webhook + * state→FAILED, stops sending events. Reset: scripts/recreate_daily_webhook.py + */ + post: operations["v1_webhook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/livekit/webhook": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Livekit Webhook + * @description Handle LiveKit webhook events. + * + * LiveKit webhook events include: + * - participant_joined / participant_left + * - egress_started / egress_updated / egress_ended + * - room_started / room_finished + * - track_published / track_unpublished + */ + post: operations["v1_livekit_webhook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/auth/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Login */ + post: operations["v1_login"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** AddEmailRecipientRequest */ + AddEmailRecipientRequest: { + /** + * Email + * Format: email + */ + email: string; + }; + /** ApiKeyResponse */ + ApiKeyResponse: { + /** + * Id + * @description A non-empty string + */ + id: string; + /** + * User Id + * @description A non-empty string + */ + user_id: string; + /** Name */ + name: string | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + }; + /** AudioWaveform */ + AudioWaveform: { + /** Data */ + data: number[]; + }; + /** Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post */ + Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post: { + /** Chunk */ + chunk: string; + }; + /** CalendarEventResponse */ + CalendarEventResponse: { + /** Id */ + id: string; + /** Room Id */ + room_id: string; + /** Ics Uid */ + ics_uid: string; + /** Title */ + title?: string | null; + /** Description */ + description?: string | null; + /** + * Start Time + * Format: date-time + */ + start_time: string; + /** + * End Time + * Format: date-time + */ + end_time: string; + /** Attendees */ + attendees?: { + [key: string]: unknown; + }[] | null; + /** Location */ + location?: string | null; + /** + * Last Synced + * Format: date-time + */ + last_synced: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Updated At + * Format: date-time + */ + updated_at: string; + }; + /** ConfigResponse */ + ConfigResponse: { + /** Zulip Enabled */ + zulip_enabled: boolean; + /** Email Enabled */ + email_enabled: boolean; + }; + /** CreateApiKeyRequest */ + CreateApiKeyRequest: { + /** Name */ + name?: string | null; + }; + /** CreateApiKeyResponse */ + CreateApiKeyResponse: { + /** + * Id + * @description A non-empty string + */ + id: string; + /** + * User Id + * @description A non-empty string + */ + user_id: string; + /** Name */ + name: string | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Key + * @description A non-empty string + */ + key: string; + }; + /** CreateParticipant */ + CreateParticipant: { + /** Speaker */ + speaker?: number | null; + /** Name */ + name: string; + }; + /** CreateRoom */ + CreateRoom: { + /** Name */ + name: string; + /** Zulip Auto Post */ + zulip_auto_post: boolean; + /** Zulip Stream */ + zulip_stream: string; + /** Zulip Topic */ + zulip_topic: string; + /** Is Locked */ + is_locked: boolean; + /** Room Mode */ + room_mode: string; + /** Recording Type */ + recording_type: string; + /** Recording Trigger */ + recording_trigger: string; + /** Is Shared */ + is_shared: boolean; + /** Webhook Url */ + webhook_url: string; + /** Webhook Secret */ + webhook_secret: string; + /** Ics Url */ + ics_url?: string | null; + /** + * Ics Fetch Interval + * @default 300 + */ + ics_fetch_interval: number; + /** + * Ics Enabled + * @default false + */ + ics_enabled: boolean; + /** + * Platform + * @enum {string} + */ + platform: "whereby" | "daily" | "livekit"; + /** + * Skip Consent + * @default false + */ + skip_consent: boolean; + /** Email Transcript To */ + email_transcript_to?: string | null; + /** + * Store Video + * @default false + */ + store_video: boolean; + }; + /** CreateRoomMeeting */ + CreateRoomMeeting: { + /** + * Allow Duplicated + * @default false + */ + allow_duplicated: boolean | null; + }; + /** CreateTranscript */ + CreateTranscript: { + /** Name */ + name: string; + /** + * Source Language + * @default en + */ + source_language: string; + /** + * Target Language + * @default en + */ + target_language: string; + source_kind?: components["schemas"]["SourceKind"] | null; + }; + /** DeletionStatus */ + DeletionStatus: { + /** Status */ + status: string; + }; + /** GetTranscriptMinimal */ + GetTranscriptMinimal: { + /** Id */ + id: string; + /** User Id */ + user_id: string | null; + /** Name */ + name: string; + /** + * Status + * @enum {string} + */ + status: "idle" | "uploaded" | "recording" | "processing" | "error" | "ended"; + /** Locked */ + locked: boolean; + /** Duration */ + duration: number; + /** Title */ + title: string | null; + /** Short Summary */ + short_summary: string | null; + /** Long Summary */ + long_summary: string | null; + /** Created At */ + created_at: string; + /** + * Share Mode + * @default private + */ + share_mode: string; + /** Source Language */ + source_language: string | null; + /** Target Language */ + target_language: string | null; + /** Reviewed */ + reviewed: boolean; + /** Meeting Id */ + meeting_id: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + /** Change Seq */ + change_seq?: number | null; + /** + * Has Cloud Video + * @default false + */ + has_cloud_video: boolean; + /** Cloud Video Duration */ + cloud_video_duration?: number | null; + /** + * Speaker Count + * @default 0 + */ + speaker_count: number; + }; + /** GetTranscriptSegmentTopic */ + GetTranscriptSegmentTopic: { + /** Text */ + text: string; + /** Start */ + start: number; + /** Speaker */ + speaker: number; + }; + /** GetTranscriptTopic */ + GetTranscriptTopic: { + /** Id */ + id: string; + /** Title */ + title: string; + /** Summary */ + summary: string; + /** Timestamp */ + timestamp: number; + /** Duration */ + duration: number | null; + /** Transcript */ + transcript: string; + /** + * Segments + * @default [] + */ + segments: components["schemas"]["GetTranscriptSegmentTopic"][]; + }; + /** GetTranscriptTopicWithWords */ + GetTranscriptTopicWithWords: { + /** Id */ + id: string; + /** Title */ + title: string; + /** Summary */ + summary: string; + /** Timestamp */ + timestamp: number; + /** Duration */ + duration: number | null; + /** Transcript */ + transcript: string; + /** + * Segments + * @default [] + */ + segments: components["schemas"]["GetTranscriptSegmentTopic"][]; + /** + * Words + * @default [] + */ + words: components["schemas"]["Word"][]; + }; + /** GetTranscriptTopicWithWordsPerSpeaker */ + GetTranscriptTopicWithWordsPerSpeaker: { + /** Id */ + id: string; + /** Title */ + title: string; + /** Summary */ + summary: string; + /** Timestamp */ + timestamp: number; + /** Duration */ + duration: number | null; + /** Transcript */ + transcript: string; + /** + * Segments + * @default [] + */ + segments: components["schemas"]["GetTranscriptSegmentTopic"][]; + /** + * Words Per Speaker + * @default [] + */ + words_per_speaker: components["schemas"]["SpeakerWords"][]; + }; + /** + * GetTranscriptWithJSON + * @description Transcript response as structured JSON segments. + * + * Format: Array of segment objects with speaker info, text, and timing. + * Example: + * [ + * { + * "speaker": 0, + * "speaker_name": "John Smith", + * "text": "Hello everyone", + * "start": 0.0, + * "end": 5.0 + * } + * ] + */ + GetTranscriptWithJSON: { + /** Id */ + id: string; + /** User Id */ + user_id: string | null; + /** Name */ + name: string; + /** + * Status + * @enum {string} + */ + status: "idle" | "uploaded" | "recording" | "processing" | "error" | "ended"; + /** Locked */ + locked: boolean; + /** Duration */ + duration: number; + /** Title */ + title: string | null; + /** Short Summary */ + short_summary: string | null; + /** Long Summary */ + long_summary: string | null; + /** Created At */ + created_at: string; + /** + * Share Mode + * @default private + */ + share_mode: string; + /** Source Language */ + source_language: string | null; + /** Target Language */ + target_language: string | null; + /** Reviewed */ + reviewed: boolean; + /** Meeting Id */ + meeting_id: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + /** Change Seq */ + change_seq?: number | null; + /** + * Has Cloud Video + * @default false + */ + has_cloud_video: boolean; + /** Cloud Video Duration */ + cloud_video_duration?: number | null; + /** + * Speaker Count + * @default 0 + */ + speaker_count: number; + /** Participants */ + participants: components["schemas"]["TranscriptParticipantWithEmail"][] | null; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + transcript_format: "json"; + /** Transcript */ + transcript: components["schemas"]["TranscriptSegment"][]; + }; + /** GetTranscriptWithParticipants */ + GetTranscriptWithParticipants: { + /** Id */ + id: string; + /** User Id */ + user_id: string | null; + /** Name */ + name: string; + /** + * Status + * @enum {string} + */ + status: "idle" | "uploaded" | "recording" | "processing" | "error" | "ended"; + /** Locked */ + locked: boolean; + /** Duration */ + duration: number; + /** Title */ + title: string | null; + /** Short Summary */ + short_summary: string | null; + /** Long Summary */ + long_summary: string | null; + /** Created At */ + created_at: string; + /** + * Share Mode + * @default private + */ + share_mode: string; + /** Source Language */ + source_language: string | null; + /** Target Language */ + target_language: string | null; + /** Reviewed */ + reviewed: boolean; + /** Meeting Id */ + meeting_id: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + /** Change Seq */ + change_seq?: number | null; + /** + * Has Cloud Video + * @default false + */ + has_cloud_video: boolean; + /** Cloud Video Duration */ + cloud_video_duration?: number | null; + /** + * Speaker Count + * @default 0 + */ + speaker_count: number; + /** Participants */ + participants: components["schemas"]["TranscriptParticipantWithEmail"][] | null; + }; + /** + * GetTranscriptWithText + * @description Transcript response with plain text format. + * + * Format: Speaker names followed by their dialogue, one line per segment. + * Example: + * John Smith: Hello everyone + * Jane Doe: Hi there + */ + GetTranscriptWithText: { + /** Id */ + id: string; + /** User Id */ + user_id: string | null; + /** Name */ + name: string; + /** + * Status + * @enum {string} + */ + status: "idle" | "uploaded" | "recording" | "processing" | "error" | "ended"; + /** Locked */ + locked: boolean; + /** Duration */ + duration: number; + /** Title */ + title: string | null; + /** Short Summary */ + short_summary: string | null; + /** Long Summary */ + long_summary: string | null; + /** Created At */ + created_at: string; + /** + * Share Mode + * @default private + */ + share_mode: string; + /** Source Language */ + source_language: string | null; + /** Target Language */ + target_language: string | null; + /** Reviewed */ + reviewed: boolean; + /** Meeting Id */ + meeting_id: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + /** Change Seq */ + change_seq?: number | null; + /** + * Has Cloud Video + * @default false + */ + has_cloud_video: boolean; + /** Cloud Video Duration */ + cloud_video_duration?: number | null; + /** + * Speaker Count + * @default 0 + */ + speaker_count: number; + /** Participants */ + participants: components["schemas"]["TranscriptParticipantWithEmail"][] | null; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + transcript_format: "text"; + /** Transcript */ + transcript: string; + }; + /** + * GetTranscriptWithTextTimestamped + * @description Transcript response with timestamped text format. + * + * Format: [MM:SS] timestamp prefix before each speaker and dialogue. + * Example: + * [00:00] John Smith: Hello everyone + * [00:05] Jane Doe: Hi there + */ + GetTranscriptWithTextTimestamped: { + /** Id */ + id: string; + /** User Id */ + user_id: string | null; + /** Name */ + name: string; + /** + * Status + * @enum {string} + */ + status: "idle" | "uploaded" | "recording" | "processing" | "error" | "ended"; + /** Locked */ + locked: boolean; + /** Duration */ + duration: number; + /** Title */ + title: string | null; + /** Short Summary */ + short_summary: string | null; + /** Long Summary */ + long_summary: string | null; + /** Created At */ + created_at: string; + /** + * Share Mode + * @default private + */ + share_mode: string; + /** Source Language */ + source_language: string | null; + /** Target Language */ + target_language: string | null; + /** Reviewed */ + reviewed: boolean; + /** Meeting Id */ + meeting_id: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + /** Change Seq */ + change_seq?: number | null; + /** + * Has Cloud Video + * @default false + */ + has_cloud_video: boolean; + /** Cloud Video Duration */ + cloud_video_duration?: number | null; + /** + * Speaker Count + * @default 0 + */ + speaker_count: number; + /** Participants */ + participants: components["schemas"]["TranscriptParticipantWithEmail"][] | null; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + transcript_format: "text-timestamped"; + /** Transcript */ + transcript: string; + }; + /** + * GetTranscriptWithWebVTTNamed + * @description Transcript response in WebVTT subtitle format with participant names. + * + * Format: Standard WebVTT with voice tags using participant names. + * Example: + * WEBVTT + * + * 00:00:00.000 --> 00:00:05.000 + * Hello everyone + */ + GetTranscriptWithWebVTTNamed: { + /** Id */ + id: string; + /** User Id */ + user_id: string | null; + /** Name */ + name: string; + /** + * Status + * @enum {string} + */ + status: "idle" | "uploaded" | "recording" | "processing" | "error" | "ended"; + /** Locked */ + locked: boolean; + /** Duration */ + duration: number; + /** Title */ + title: string | null; + /** Short Summary */ + short_summary: string | null; + /** Long Summary */ + long_summary: string | null; + /** Created At */ + created_at: string; + /** + * Share Mode + * @default private + */ + share_mode: string; + /** Source Language */ + source_language: string | null; + /** Target Language */ + target_language: string | null; + /** Reviewed */ + reviewed: boolean; + /** Meeting Id */ + meeting_id: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + /** Change Seq */ + change_seq?: number | null; + /** + * Has Cloud Video + * @default false + */ + has_cloud_video: boolean; + /** Cloud Video Duration */ + cloud_video_duration?: number | null; + /** + * Speaker Count + * @default 0 + */ + speaker_count: number; + /** Participants */ + participants: components["schemas"]["TranscriptParticipantWithEmail"][] | null; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + transcript_format: "webvtt-named"; + /** Transcript */ + transcript: string; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** ICSStatus */ + ICSStatus: { + /** + * Status + * @enum {string} + */ + status: "enabled" | "disabled"; + /** Last Sync */ + last_sync?: string | null; + /** Next Sync */ + next_sync?: string | null; + /** Last Etag */ + last_etag?: string | null; + /** + * Events Count + * @default 0 + */ + events_count: number; + }; + /** ICSSyncResult */ + ICSSyncResult: { + status: components["schemas"]["SyncStatus"]; + /** Hash */ + hash?: string | null; + /** + * Events Found + * @default 0 + */ + events_found: number; + /** + * Total Events + * @default 0 + */ + total_events: number; + /** + * Events Created + * @default 0 + */ + events_created: number; + /** + * Events Updated + * @default 0 + */ + events_updated: number; + /** + * Events Deleted + * @default 0 + */ + events_deleted: number; + /** Error */ + error?: string | null; + /** Reason */ + reason?: string | null; + }; + /** LoginRequest */ + LoginRequest: { + /** Email */ + email: string; + /** Password */ + password: string; + }; + /** LoginResponse */ + LoginResponse: { + /** Access Token */ + access_token: string; + /** + * Token Type + * @default bearer + */ + token_type: string; + /** Expires In */ + expires_in: number; + }; + /** Meeting */ + Meeting: { + /** Id */ + id: string; + /** Room Name */ + room_name: string; + /** Room Url */ + room_url: string; + /** Host Room Url */ + host_room_url: string; + /** + * Start Date + * Format: date-time + */ + start_date: string; + /** + * End Date + * Format: date-time + */ + end_date: string; + /** User Id */ + user_id?: string | null; + /** Room Id */ + room_id?: string | null; + /** + * Is Locked + * @default false + */ + is_locked: boolean; + /** + * Room Mode + * @default normal + * @enum {string} + */ + room_mode: "normal" | "group"; + /** + * Recording Type + * @default cloud + * @enum {string} + */ + recording_type: "none" | "local" | "cloud"; + /** + * Recording Trigger + * @default automatic-2nd-participant + * @enum {string} + */ + recording_trigger: "none" | "prompt" | "automatic" | "automatic-2nd-participant"; + /** + * Num Clients + * @default 0 + */ + num_clients: number; + /** + * Is Active + * @default true + */ + is_active: boolean; + /** Calendar Event Id */ + calendar_event_id?: string | null; + /** Calendar Metadata */ + calendar_metadata?: { + [key: string]: unknown; + } | null; + /** + * Platform + * @enum {string} + */ + platform: "whereby" | "daily" | "livekit"; + /** Daily Composed Video S3 Key */ + daily_composed_video_s3_key?: string | null; + /** Daily Composed Video Duration */ + daily_composed_video_duration?: number | null; + /** + * Store Video + * @default false + */ + store_video: boolean; + }; + /** MeetingConsentRequest */ + MeetingConsentRequest: { + /** Consent Given */ + consent_given: boolean; + }; + /** Page[GetTranscriptMinimal] */ + Page_GetTranscriptMinimal_: { + /** Items */ + items: components["schemas"]["GetTranscriptMinimal"][]; + /** Total */ + total: number; + /** Page */ + page: number; + /** Size */ + size: number; + /** Pages */ + pages: number; + }; + /** Page[RoomDetails] */ + Page_RoomDetails_: { + /** Items */ + items: components["schemas"]["RoomDetails"][]; + /** Total */ + total: number; + /** Page */ + page: number; + /** Size */ + size: number; + /** Pages */ + pages: number; + }; + /** Participant */ + Participant: { + /** Id */ + id: string; + /** Speaker */ + speaker: number | null; + /** Name */ + name: string; + }; + /** ProcessStatus */ + ProcessStatus: { + /** Status */ + status: string; + }; + /** Room */ + Room: { + /** Id */ + id: string; + /** Name */ + name: string; + /** User Id */ + user_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Zulip Auto Post */ + zulip_auto_post: boolean; + /** Zulip Stream */ + zulip_stream: string; + /** Zulip Topic */ + zulip_topic: string; + /** Is Locked */ + is_locked: boolean; + /** Room Mode */ + room_mode: string; + /** Recording Type */ + recording_type: string; + /** Recording Trigger */ + recording_trigger: string; + /** Is Shared */ + is_shared: boolean; + /** Ics Url */ + ics_url?: string | null; + /** + * Ics Fetch Interval + * @default 300 + */ + ics_fetch_interval: number; + /** + * Ics Enabled + * @default false + */ + ics_enabled: boolean; + /** Ics Last Sync */ + ics_last_sync?: string | null; + /** Ics Last Etag */ + ics_last_etag?: string | null; + /** + * Platform + * @enum {string} + */ + platform: "whereby" | "daily" | "livekit"; + /** + * Skip Consent + * @default false + */ + skip_consent: boolean; + /** Email Transcript To */ + email_transcript_to?: string | null; + /** + * Store Video + * @default false + */ + store_video: boolean; + }; + /** RoomDetails */ + RoomDetails: { + /** Id */ + id: string; + /** Name */ + name: string; + /** User Id */ + user_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Zulip Auto Post */ + zulip_auto_post: boolean; + /** Zulip Stream */ + zulip_stream: string; + /** Zulip Topic */ + zulip_topic: string; + /** Is Locked */ + is_locked: boolean; + /** Room Mode */ + room_mode: string; + /** Recording Type */ + recording_type: string; + /** Recording Trigger */ + recording_trigger: string; + /** Is Shared */ + is_shared: boolean; + /** Ics Url */ + ics_url?: string | null; + /** + * Ics Fetch Interval + * @default 300 + */ + ics_fetch_interval: number; + /** + * Ics Enabled + * @default false + */ + ics_enabled: boolean; + /** Ics Last Sync */ + ics_last_sync?: string | null; + /** Ics Last Etag */ + ics_last_etag?: string | null; + /** + * Platform + * @enum {string} + */ + platform: "whereby" | "daily" | "livekit"; + /** + * Skip Consent + * @default false + */ + skip_consent: boolean; + /** Email Transcript To */ + email_transcript_to?: string | null; + /** + * Store Video + * @default false + */ + store_video: boolean; + /** Webhook Url */ + webhook_url: string | null; + /** Webhook Secret */ + webhook_secret: string | null; + }; + /** RtcOffer */ + RtcOffer: { + /** Sdp */ + sdp: string; + /** Type */ + type: string; + }; + /** SearchResponse */ + SearchResponse: { + /** Results */ + results: components["schemas"]["SearchResult"][]; + /** + * Total + * @description Total number of search results + */ + total: number; + /** Query */ + query?: string | null; + /** + * Limit + * @description Results per page + */ + limit: number; + /** + * Offset + * @description Number of results to skip + */ + offset: number; + }; + /** + * SearchResult + * @description Public search result model with computed fields. + */ + SearchResult: { + /** Id */ + id: string; + /** Title */ + title?: string | null; + /** User Id */ + user_id?: string | null; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Created At */ + created_at: string; + /** + * Status + * @enum {string} + */ + status: "idle" | "uploaded" | "recording" | "processing" | "error" | "ended"; + /** Rank */ + rank: number; + /** + * Duration + * @description Duration in seconds + */ + duration: number | null; + /** + * Search Snippets + * @description Text snippets around search matches + */ + search_snippets: string[]; + /** + * Total Match Count + * @description Total number of matches found in the transcript + * @default 0 + */ + total_match_count: number; + /** + * Speaker Count + * @description Number of distinct speakers in the transcript + * @default 0 + */ + speaker_count: number; + /** Change Seq */ + change_seq?: number | null; + }; + /** SendEmailRequest */ + SendEmailRequest: { + /** Email */ + email: string; + }; + /** SendEmailResponse */ + SendEmailResponse: { + /** Sent */ + sent: number; + }; + /** + * SourceKind + * @enum {string} + */ + SourceKind: "room" | "live" | "file"; + /** SpeakerAssignment */ + SpeakerAssignment: { + /** Speaker */ + speaker?: number | null; + /** Participant */ + participant?: string | null; + /** Timestamp From */ + timestamp_from: number; + /** Timestamp To */ + timestamp_to: number; + }; + /** SpeakerAssignmentStatus */ + SpeakerAssignmentStatus: { + /** Status */ + status: string; + }; + /** SpeakerMerge */ + SpeakerMerge: { + /** Speaker From */ + speaker_from: number; + /** Speaker To */ + speaker_to: number; + }; + /** SpeakerWords */ + SpeakerWords: { + /** Speaker */ + speaker: number; + /** Words */ + words: components["schemas"]["Word"][]; + }; + /** StartRecordingRequest */ + StartRecordingRequest: { + /** + * Type + * @enum {string} + */ + type: "cloud" | "raw-tracks"; + /** + * Instanceid + * Format: uuid + */ + instanceId: string; + }; + /** Stream */ + Stream: { + /** Stream Id */ + stream_id: number; + /** Name */ + name: string; + }; + /** + * SyncStatus + * @enum {string} + */ + SyncStatus: "success" | "unchanged" | "error" | "skipped"; + /** Topic */ + Topic: { + /** Name */ + name: string; + }; + /** TranscriptActionItems */ + TranscriptActionItems: { + /** Action Items */ + action_items: { + [key: string]: unknown; + }; + }; + /** TranscriptDuration */ + TranscriptDuration: { + /** Duration */ + duration: number; + }; + /** TranscriptFinalLongSummary */ + TranscriptFinalLongSummary: { + /** Long Summary */ + long_summary: string; + }; + /** TranscriptFinalShortSummary */ + TranscriptFinalShortSummary: { + /** Short Summary */ + short_summary: string; + }; + /** TranscriptFinalTitle */ + TranscriptFinalTitle: { + /** Title */ + title: string; + }; + /** TranscriptParticipant */ + TranscriptParticipant: { + /** Id */ + id?: string; + /** Speaker */ + speaker: number | null; + /** Name */ + name: string; + /** User Id */ + user_id?: string | null; + }; + /** TranscriptParticipantWithEmail */ + TranscriptParticipantWithEmail: { + /** Id */ + id?: string; + /** Speaker */ + speaker: number | null; + /** Name */ + name: string; + /** User Id */ + user_id?: string | null; + /** Email */ + email?: string | null; + }; + /** + * TranscriptSegment + * @description A single transcript segment with speaker and timing information. + */ + TranscriptSegment: { + /** Speaker */ + speaker: number; + /** Speaker Name */ + speaker_name: string; + /** Text */ + text: string; + /** Start */ + start: number; + /** End */ + end: number; + }; + /** TranscriptText */ + TranscriptText: { + /** Text */ + text: string; + /** Translation */ + translation: string | null; + }; + /** TranscriptWaveform */ + TranscriptWaveform: { + /** Waveform */ + waveform: number[]; + }; + /** TranscriptWsActionItems */ + TranscriptWsActionItems: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "ACTION_ITEMS"; + data: components["schemas"]["TranscriptActionItems"]; + }; + /** TranscriptWsDuration */ + TranscriptWsDuration: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "DURATION"; + data: components["schemas"]["TranscriptDuration"]; + }; + /** TranscriptWsFinalLongSummary */ + TranscriptWsFinalLongSummary: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "FINAL_LONG_SUMMARY"; + data: components["schemas"]["TranscriptFinalLongSummary"]; + }; + /** TranscriptWsFinalShortSummary */ + TranscriptWsFinalShortSummary: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "FINAL_SHORT_SUMMARY"; + data: components["schemas"]["TranscriptFinalShortSummary"]; + }; + /** TranscriptWsFinalTitle */ + TranscriptWsFinalTitle: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "FINAL_TITLE"; + data: components["schemas"]["TranscriptFinalTitle"]; + }; + /** TranscriptWsStatus */ + TranscriptWsStatus: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "STATUS"; + data: components["schemas"]["TranscriptWsStatusData"]; + }; + /** TranscriptWsStatusData */ + TranscriptWsStatusData: { + /** + * Value + * @enum {string} + */ + value: "idle" | "uploaded" | "recording" | "processing" | "error" | "ended"; + }; + /** TranscriptWsTopic */ + TranscriptWsTopic: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "TOPIC"; + data: components["schemas"]["GetTranscriptTopic"]; + }; + /** TranscriptWsTranscript */ + TranscriptWsTranscript: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "TRANSCRIPT"; + data: components["schemas"]["TranscriptText"]; + }; + /** TranscriptWsWaveform */ + TranscriptWsWaveform: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "WAVEFORM"; + data: components["schemas"]["TranscriptWaveform"]; + }; + /** UpdateParticipant */ + UpdateParticipant: { + /** Speaker */ + speaker?: number | null; + /** Name */ + name?: string | null; + }; + /** UpdateRoom */ + UpdateRoom: { + /** Name */ + name?: string | null; + /** Zulip Auto Post */ + zulip_auto_post?: boolean | null; + /** Zulip Stream */ + zulip_stream?: string | null; + /** Zulip Topic */ + zulip_topic?: string | null; + /** Is Locked */ + is_locked?: boolean | null; + /** Room Mode */ + room_mode?: string | null; + /** Recording Type */ + recording_type?: string | null; + /** Recording Trigger */ + recording_trigger?: string | null; + /** Is Shared */ + is_shared?: boolean | null; + /** Webhook Url */ + webhook_url?: string | null; + /** Webhook Secret */ + webhook_secret?: string | null; + /** Ics Url */ + ics_url?: string | null; + /** Ics Fetch Interval */ + ics_fetch_interval?: number | null; + /** Ics Enabled */ + ics_enabled?: boolean | null; + /** Platform */ + platform?: ("whereby" | "daily" | "livekit") | null; + /** Skip Consent */ + skip_consent?: boolean | null; + /** Email Transcript To */ + email_transcript_to?: string | null; + /** Store Video */ + store_video?: boolean | null; + }; + /** UpdateTranscript */ + UpdateTranscript: { + /** Name */ + name?: string | null; + /** Locked */ + locked?: boolean | null; + /** Title */ + title?: string | null; + /** Short Summary */ + short_summary?: string | null; + /** Long Summary */ + long_summary?: string | null; + /** Share Mode */ + share_mode?: ("public" | "semi-private" | "private") | null; + /** Participants */ + participants?: components["schemas"]["TranscriptParticipant"][] | null; + /** Reviewed */ + reviewed?: boolean | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + }; + /** UserInfo */ + UserInfo: { + /** Sub */ + sub: string; + /** Email */ + email: string | null; + }; + /** UserTranscriptCreatedData */ + UserTranscriptCreatedData: { + /** + * Id + * @description A non-empty string + */ + id: string; + }; + /** UserTranscriptDeletedData */ + UserTranscriptDeletedData: { + /** + * Id + * @description A non-empty string + */ + id: string; + }; + /** UserTranscriptDurationData */ + UserTranscriptDurationData: { + /** + * Id + * @description A non-empty string + */ + id: string; + /** Duration */ + duration: number; + }; + /** UserTranscriptFinalTitleData */ + UserTranscriptFinalTitleData: { + /** + * Id + * @description A non-empty string + */ + id: string; + /** + * Title + * @description A non-empty string + */ + title: string; + }; + /** UserTranscriptRestoredData */ + UserTranscriptRestoredData: { + /** + * Id + * @description A non-empty string + */ + id: string; + }; + /** UserTranscriptStatusData */ + UserTranscriptStatusData: { + /** + * Id + * @description A non-empty string + */ + id: string; + /** + * Value + * @enum {string} + */ + value: "idle" | "uploaded" | "recording" | "processing" | "error" | "ended"; + }; + /** UserWsTranscriptCreated */ + UserWsTranscriptCreated: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "TRANSCRIPT_CREATED"; + data: components["schemas"]["UserTranscriptCreatedData"]; + }; + /** UserWsTranscriptDeleted */ + UserWsTranscriptDeleted: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "TRANSCRIPT_DELETED"; + data: components["schemas"]["UserTranscriptDeletedData"]; + }; + /** UserWsTranscriptDuration */ + UserWsTranscriptDuration: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "TRANSCRIPT_DURATION"; + data: components["schemas"]["UserTranscriptDurationData"]; + }; + /** UserWsTranscriptFinalTitle */ + UserWsTranscriptFinalTitle: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "TRANSCRIPT_FINAL_TITLE"; + data: components["schemas"]["UserTranscriptFinalTitleData"]; + }; + /** UserWsTranscriptRestored */ + UserWsTranscriptRestored: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "TRANSCRIPT_RESTORED"; + data: components["schemas"]["UserTranscriptRestoredData"]; + }; + /** UserWsTranscriptStatus */ + UserWsTranscriptStatus: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + event: "TRANSCRIPT_STATUS"; + data: components["schemas"]["UserTranscriptStatusData"]; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + /** Input */ + input?: unknown; + /** Context */ + ctx?: Record; + }; + /** VideoUrlResponse */ + VideoUrlResponse: { + /** Url */ + url: string; + /** Duration */ + duration?: number | null; + /** + * Content Type + * @default video/mp4 + */ + content_type: string; + }; + /** WebhookTestResult */ + WebhookTestResult: { + /** Success */ + success: boolean; + /** + * Message + * @default + */ + message: string; + /** + * Error + * @default + */ + error: string; + /** Status Code */ + status_code?: number | null; + /** Response Preview */ + response_preview?: string | null; + }; + /** WherebyWebhookEvent */ + WherebyWebhookEvent: { + /** Apiversion */ + apiVersion: string; + /** Id */ + id: string; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** Type */ + type: string; + /** Data */ + data: { + [key: string]: unknown; + }; + }; + /** Word */ + Word: { + /** Text */ + text: string; + /** + * Start + * @description Time in seconds with float part + */ + start: number; + /** + * End + * @description Time in seconds with float part + */ + end: number; + /** + * Speaker + * @default 0 + */ + speaker: number; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + health: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + }; + }; + metrics: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + }; + }; + v1_meeting_audio_consent: { + parameters: { + query?: never; + header?: never; + path: { + meeting_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MeetingConsentRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_meeting_deactivate: { + parameters: { + query?: never; + header?: never; + path: { + meeting_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_start_recording: { + parameters: { + query?: never; + header?: never; + path: { + meeting_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["StartRecordingRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_add_email_recipient: { + parameters: { + query?: never; + header?: never; + path: { + meeting_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AddEmailRecipientRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_list: { + parameters: { + query?: { + /** @description Page number */ + page?: number; + /** @description Page size */ + size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Page_RoomDetails_"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateRoom"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Room"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_get: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RoomDetails"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_delete: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletionStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_update: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateRoom"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RoomDetails"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_get_by_name: { + parameters: { + query?: never; + header?: never; + path: { + room_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RoomDetails"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_create_meeting: { + parameters: { + query?: never; + header?: never; + path: { + room_name: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateRoomMeeting"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Meeting"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_test_webhook: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookTestResult"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_sync_ics: { + parameters: { + query?: never; + header?: never; + path: { + room_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ICSSyncResult"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_ics_status: { + parameters: { + query?: never; + header?: never; + path: { + room_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ICSStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_list_meetings: { + parameters: { + query?: never; + header?: never; + path: { + room_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CalendarEventResponse"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_list_upcoming_meetings: { + parameters: { + query?: { + minutes_ahead?: number; + }; + header?: never; + path: { + room_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CalendarEventResponse"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_list_active_meetings: { + parameters: { + query?: never; + header?: never; + path: { + room_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Meeting"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_get_meeting: { + parameters: { + query?: never; + header?: never; + path: { + room_name: string; + meeting_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Meeting"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_join_meeting: { + parameters: { + query?: { + display_name?: string | null; + }; + header?: never; + path: { + room_name: string; + meeting_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Meeting"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcripts_list: { + parameters: { + query?: { + source_kind?: components["schemas"]["SourceKind"] | null; + room_id?: string | null; + search_term?: string | null; + change_seq_from?: number | null; + sort_by?: ("created_at" | "change_seq") | null; + /** @description Page number */ + page?: number; + /** @description Page size */ + size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Page_GetTranscriptMinimal_"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcripts_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateTranscript"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscriptWithParticipants"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcripts_search: { + parameters: { + query: { + /** @description Search query text */ + q: string; + /** @description Results per page */ + limit?: number; + /** @description Number of results to skip */ + offset?: number; + room_id?: string | null; + source_kind?: components["schemas"]["SourceKind"] | null; + /** @description Filter transcripts created on or after this datetime (ISO 8601 with timezone) */ + from?: string | null; + /** @description Filter transcripts created on or before this datetime (ISO 8601 with timezone) */ + to?: string | null; + include_deleted?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SearchResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get: { + parameters: { + query?: { + transcript_format?: "text" | "text-timestamped" | "webvtt-named" | "json"; + }; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscriptWithText"] | components["schemas"]["GetTranscriptWithTextTimestamped"] | components["schemas"]["GetTranscriptWithWebVTTNamed"] | components["schemas"]["GetTranscriptWithJSON"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_delete: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletionStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_update: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateTranscript"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscriptWithParticipants"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_restore: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletionStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_destroy: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletionStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_topics: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscriptTopic"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_topics_with_words: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscriptTopicWithWords"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_topics_with_words_per_speaker: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + topic_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscriptTopicWithWordsPerSpeaker"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_post_to_zulip: { + parameters: { + query: { + stream: string; + topic: string; + include_topics: boolean; + }; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_send_email: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SendEmailRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SendEmailResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_audio_mp3: { + parameters: { + query?: { + token?: string | null; + }; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_head_audio_mp3: { + parameters: { + query?: { + token?: string | null; + }; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_audio_waveform: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AudioWaveform"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_participants: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Participant"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_add_participant: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateParticipant"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Participant"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_participant: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + participant_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Participant"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_delete_participant: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + participant_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletionStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_update_participant: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + participant_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateParticipant"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Participant"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_assign_speaker: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SpeakerAssignment"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SpeakerAssignmentStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_merge_speaker: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SpeakerMerge"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SpeakerAssignmentStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_record_upload: { + parameters: { + query: { + chunk_number: number; + total_chunks: number; + }; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_download_zip: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_video_url: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["VideoUrlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_websocket_events: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranscriptWsTranscript"] | components["schemas"]["TranscriptWsTopic"] | components["schemas"]["TranscriptWsStatus"] | components["schemas"]["TranscriptWsFinalTitle"] | components["schemas"]["TranscriptWsFinalLongSummary"] | components["schemas"]["TranscriptWsFinalShortSummary"] | components["schemas"]["TranscriptWsActionItems"] | components["schemas"]["TranscriptWsDuration"] | components["schemas"]["TranscriptWsWaveform"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_record_webrtc: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RtcOffer"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_process: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProcessStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_user_me: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfo"] | null; + }; + }; + }; + }; + v1_list_api_keys: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiKeyResponse"][]; + }; + }; + }; + }; + v1_create_api_key: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateApiKeyRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateApiKeyResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_delete_api_key: { + parameters: { + query?: never; + header?: never; + path: { + key_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_user_get_websocket_events: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserWsTranscriptCreated"] | components["schemas"]["UserWsTranscriptDeleted"] | components["schemas"]["UserWsTranscriptRestored"] | components["schemas"]["UserWsTranscriptStatus"] | components["schemas"]["UserWsTranscriptFinalTitle"] | components["schemas"]["UserWsTranscriptDuration"]; + }; + }; + }; + }; + v1_get_config: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConfigResponse"]; + }; + }; + }; + }; + v1_zulip_get_streams: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Stream"][]; + }; + }; + }; + }; + v1_zulip_get_topics: { + parameters: { + query?: never; + header?: never; + path: { + stream_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Topic"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_whereby_webhook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WherebyWebhookEvent"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_webhook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + }; + }; + v1_livekit_webhook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + }; + }; + v1_login: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LoginResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; +} diff --git a/ui/src/auth/AuthContext.tsx b/ui/src/auth/AuthContext.tsx new file mode 100644 index 00000000..041e4232 --- /dev/null +++ b/ui/src/auth/AuthContext.tsx @@ -0,0 +1,28 @@ +import { createContext, useContext } from 'react' + +export type AuthMode = 'oidc' | 'password' + +export type AuthUser = { + email?: string | null + name?: string | null + sub?: string | null +} | null + +export type AuthContextValue = { + mode: AuthMode + loading: boolean + authenticated: boolean + user: AuthUser + error: Error | null + loginWithOidc: () => void + loginWithPassword: (email: string, password: string) => Promise + logout: () => Promise +} + +export const AuthContext = createContext(null) + +export function useAuth(): AuthContextValue { + const value = useContext(AuthContext) + if (!value) throw new Error('useAuth must be used inside AuthProvider') + return value +} diff --git a/ui/src/auth/AuthProvider.tsx b/ui/src/auth/AuthProvider.tsx new file mode 100644 index 00000000..f797dcf3 --- /dev/null +++ b/ui/src/auth/AuthProvider.tsx @@ -0,0 +1,129 @@ +import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react' +import { + AuthProvider as OidcAuthProvider, + useAuth as useOidcAuth, +} from 'react-oidc-context' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { apiClient, getPasswordToken, setPasswordToken, setOidcAccessTokenGetter } from '@/api/client' +import { AuthContext, type AuthContextValue, type AuthUser } from './AuthContext' +import { buildOidcConfig, oidcEnabled } from './oidcConfig' + +function useMeQuery(tokenKey: string | null | undefined) { + return useQuery({ + queryKey: ['auth', 'me', tokenKey ?? 'anon'], + enabled: !!tokenKey, + queryFn: async () => { + const { data, error, response } = await apiClient.GET('/v1/me') + if (error || !response.ok) { + if (response.status === 401) return null + throw Object.assign(new Error('me request failed'), { status: response.status }) + } + return (data ?? null) as AuthUser + }, + staleTime: 60_000, + }) +} + +function PasswordAuthProvider({ children }: { children: ReactNode }) { + const queryClient = useQueryClient() + const [token, setToken] = useState(() => getPasswordToken()) + const meQuery = useMeQuery(token) + + const loginWithPassword = useCallback( + async (email: string, password: string) => { + const res = await fetch('/v1/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }) + if (!res.ok) { + const detail = await res + .json() + .then((j: { detail?: string }) => j?.detail) + .catch(() => null) + throw new Error(detail ?? 'Invalid credentials') + } + const json = (await res.json()) as { access_token: string } + setPasswordToken(json.access_token) + setToken(json.access_token) + await queryClient.invalidateQueries({ queryKey: ['auth', 'me'] }) + }, + [queryClient], + ) + + const logout = useCallback(async () => { + setPasswordToken(null) + setToken(null) + await queryClient.invalidateQueries({ queryKey: ['auth', 'me'] }) + }, [queryClient]) + + const loginWithOidc = useCallback(() => { + console.warn('OIDC login not configured; use loginWithPassword') + }, []) + + const value = useMemo( + () => ({ + mode: 'password', + loading: meQuery.isLoading, + authenticated: !!token && meQuery.data != null, + user: meQuery.data ?? null, + error: (meQuery.error as Error | null) ?? null, + loginWithOidc, + loginWithPassword, + logout, + }), + [token, meQuery.isLoading, meQuery.data, meQuery.error, loginWithOidc, loginWithPassword, logout], + ) + + return {children} +} + +function OidcAuthBridge({ children }: { children: ReactNode }) { + const oidc = useOidcAuth() + const queryClient = useQueryClient() + const accessToken = oidc.user?.access_token ?? null + + useEffect(() => { + setOidcAccessTokenGetter(() => accessToken) + return () => setOidcAccessTokenGetter(null) + }, [accessToken]) + + const meQuery = useMeQuery(accessToken) + + const loginWithOidc = useCallback(() => oidc.signinRedirect(), [oidc]) + const loginWithPassword = useCallback(async () => { + throw new Error('Password login is disabled in OIDC mode') + }, []) + const logout = useCallback(async () => { + await oidc.signoutRedirect().catch(() => oidc.removeUser()) + await queryClient.invalidateQueries({ queryKey: ['auth', 'me'] }) + }, [oidc, queryClient]) + + const value = useMemo( + () => ({ + mode: 'oidc', + loading: oidc.isLoading || meQuery.isLoading, + authenticated: !!accessToken && meQuery.data != null, + user: meQuery.data ?? null, + error: (oidc.error ?? (meQuery.error as Error | null)) ?? null, + loginWithOidc, + loginWithPassword, + logout, + }), + [oidc.isLoading, oidc.error, accessToken, meQuery.isLoading, meQuery.data, meQuery.error, loginWithOidc, loginWithPassword, logout], + ) + + return {children} +} + +export function AuthProvider({ children }: { children: ReactNode }) { + const config = buildOidcConfig() + if (!config || !oidcEnabled) { + return {children} + } + return ( + + {children} + + ) +} diff --git a/ui/src/auth/RequireAuth.tsx b/ui/src/auth/RequireAuth.tsx new file mode 100644 index 00000000..ecaca665 --- /dev/null +++ b/ui/src/auth/RequireAuth.tsx @@ -0,0 +1,30 @@ +import { type ReactNode } from 'react' +import { Navigate, useLocation } from 'react-router-dom' +import { useAuth } from './AuthContext' + +export function RequireAuth({ children }: { children: ReactNode }) { + const { loading, authenticated } = useAuth() + const location = useLocation() + + if (loading) { + return ( +
+ Loading… +
+ ) + } + + if (!authenticated) { + return + } + + return <>{children} +} diff --git a/ui/src/auth/oidcConfig.ts b/ui/src/auth/oidcConfig.ts new file mode 100644 index 00000000..f1e6a51f --- /dev/null +++ b/ui/src/auth/oidcConfig.ts @@ -0,0 +1,27 @@ +import type { AuthProviderProps } from 'react-oidc-context' +import { WebStorageStateStore } from 'oidc-client-ts' +import { env, oidcEnabled } from '@/lib/env' + +export { oidcEnabled } + +export function buildOidcConfig(): AuthProviderProps | null { + if (!oidcEnabled) return null + const redirectUri = `${window.location.origin}/v2/auth/callback` + const silentRedirectUri = `${window.location.origin}/v2/auth/silent-renew` + const postLogoutRedirectUri = `${window.location.origin}/v2/` + return { + authority: env.oidcAuthority, + client_id: env.oidcClientId, + redirect_uri: redirectUri, + silent_redirect_uri: silentRedirectUri, + post_logout_redirect_uri: postLogoutRedirectUri, + scope: env.oidcScope, + response_type: 'code', + loadUserInfo: true, + automaticSilentRenew: true, + userStore: new WebStorageStateStore({ store: window.sessionStorage }), + onSigninCallback: () => { + window.history.replaceState({}, document.title, '/v2/') + }, + } +} diff --git a/ui/src/components/browse/ConfirmDialog.tsx b/ui/src/components/browse/ConfirmDialog.tsx new file mode 100644 index 00000000..8aa48055 --- /dev/null +++ b/ui/src/components/browse/ConfirmDialog.tsx @@ -0,0 +1,130 @@ +import { useEffect, type ReactNode } from 'react' +import { I } from '@/components/icons' +import { Button } from '@/components/ui/primitives' + +type Props = { + title: string + message: ReactNode + confirmLabel: string + cancelLabel?: string + danger?: boolean + loading?: boolean + onConfirm: () => void + onClose: () => void +} + +export function ConfirmDialog({ + title, + message, + confirmLabel, + cancelLabel = 'Cancel', + danger, + loading, + onConfirm, + onClose, +}: Props) { + useEffect(() => { + const k = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !loading) onClose() + } + document.addEventListener('keydown', k) + return () => document.removeEventListener('keydown', k) + }, [onClose, loading]) + + return ( + <> +
!loading && onClose()} /> +
+
+
+
+ {I.Trash(18)} +
+
+

+ {title} +

+
+ {message} +
+
+
+
+
+ + +
+
+ + ) +} diff --git a/ui/src/components/browse/FilterBar.tsx b/ui/src/components/browse/FilterBar.tsx new file mode 100644 index 00000000..5bc2bcf5 --- /dev/null +++ b/ui/src/components/browse/FilterBar.tsx @@ -0,0 +1,127 @@ +import { I } from '@/components/icons' +import type { RoomRowData, SidebarFilter, TagRowData } from '@/lib/types' + +type SortKey = 'newest' | 'oldest' | 'longest' + +type FilterBarProps = { + filter: SidebarFilter + rooms: RoomRowData[] + tags: TagRowData[] + total: number + sort: SortKey + onSort: (s: SortKey) => void + query: string + onSearch: (v: string) => void +} + +export function FilterBar({ + filter, + rooms, + tags, + total, + sort, + onSort, + query, + onSearch, +}: FilterBarProps) { + let label = 'All transcripts' + if (filter.kind === 'source' && filter.value === 'live') label = 'Live transcripts' + if (filter.kind === 'source' && filter.value === 'file') label = 'Uploaded files' + if (filter.kind === 'room') { + const r = rooms.find((x) => x.id === filter.value) + label = r ? `Room · ${r.name}` : 'Room' + } + if (filter.kind === 'tag') { + const t = tags.find((x) => x.id === filter.value) + label = t ? `Tagged · #${t.name}` : 'Tag' + } + if (filter.kind === 'trash') label = 'Trash' + if (filter.kind === 'recent') label = 'Recent (last 7 days)' + + return ( +
+ {label} + + {total} {total === 1 ? 'result' : 'results'} + +
+ {I.Search(13)} + onSearch(e.target.value)} + placeholder="Search transcripts, speakers, rooms…" + style={{ + border: 'none', + outline: 'none', + background: 'transparent', + fontFamily: 'var(--font-sans)', + fontSize: 12.5, + color: 'var(--fg)', + flex: 1, + }} + /> + ⌘K +
+
+ + sort + + {(['newest', 'oldest', 'longest'] as const).map((s) => ( + + ))} +
+ ) +} diff --git a/ui/src/components/browse/Pagination.tsx b/ui/src/components/browse/Pagination.tsx new file mode 100644 index 00000000..61922369 --- /dev/null +++ b/ui/src/components/browse/Pagination.tsx @@ -0,0 +1,74 @@ +import { I } from '@/components/icons' +import { Button } from '@/components/ui/primitives' + +type Props = { + page: number + total: number + pageSize: number + onPage: (n: number) => void +} + +export function Pagination({ page, total, pageSize, onPage }: Props) { + const totalPages = Math.max(1, Math.ceil(total / pageSize)) + if (totalPages <= 1) return null + const start = (page - 1) * pageSize + 1 + const end = Math.min(total, page * pageSize) + const pages = Array.from({ length: totalPages }, (_, i) => i + 1) + return ( +
+ + {start}–{end} of {total} + +
+ + {pages.map((n) => ( + + ))} + +
+
+ ) +} diff --git a/ui/src/components/browse/TranscriptRow.tsx b/ui/src/components/browse/TranscriptRow.tsx new file mode 100644 index 00000000..080e8f9a --- /dev/null +++ b/ui/src/components/browse/TranscriptRow.tsx @@ -0,0 +1,308 @@ +import { type ReactNode } from 'react' +import { I } from '@/components/icons' +import { RowMenuTrigger } from '@/components/ui/primitives' +import { fmtDate, fmtDur } from '@/lib/format' +import type { TranscriptRowData } from '@/lib/types' + +type Props = { + t: TranscriptRowData + active?: boolean + onSelect?: (id: string) => void + query?: string + density?: 'compact' | 'comfortable' + onDelete?: (t: TranscriptRowData) => void + onReprocess?: (id: string) => void +} + +type ApiStatus = 'recording' | 'ended' | 'processing' | 'uploaded' | 'error' | 'idle' + +const STATUS_MAP: Record = { + live: 'recording', + ended: 'ended', + processing: 'processing', + uploading: 'uploaded', + failed: 'error', + idle: 'idle', +} + +function statusIconFor(apiStatus: ApiStatus): { node: ReactNode; color: string } { + switch (apiStatus) { + case 'recording': + return { node: I.Radio(14), color: 'var(--status-live)' } + case 'processing': + return { + node: ( + + ), + color: 'var(--status-processing)', + } + case 'uploaded': + return { node: I.Clock(14), color: 'var(--fg-muted)' } + case 'error': + return { node: I.AlertTriangle(14), color: 'var(--destructive)' } + case 'ended': + return { node: I.CheckCircle(14), color: 'var(--status-ok)' } + default: + return { node: I.Clock(14), color: 'var(--fg-muted)' } + } +} + +function buildRowMenu( + t: TranscriptRowData, + onDelete?: (t: TranscriptRowData) => void, + onReprocess?: (id: string) => void, +) { + const apiStatus = STATUS_MAP[t.status] ?? 'idle' + const canReprocess = apiStatus === 'ended' || apiStatus === 'error' + return [ + { label: 'Open', icon: I.ExternalLink(14) }, + { label: 'Rename', icon: I.Edit(14) }, + { separator: true as const }, + { + label: 'Reprocess', + icon: I.Refresh(14), + disabled: !canReprocess, + onClick: () => onReprocess?.(t.id), + }, + { separator: true as const }, + { + label: 'Delete', + icon: I.Trash(14), + danger: true, + onClick: () => onDelete?.(t), + }, + ] +} + +function Highlight({ text, query }: { text: string; query?: string }) { + if (!query || !text) return <>{text} + const i = text.toLowerCase().indexOf(query.toLowerCase()) + if (i < 0) return <>{text} + return ( + <> + {text.slice(0, i)} + + {text.slice(i, i + query.length)} + + {text.slice(i + query.length)} + + ) +} + +export function TranscriptRow({ + t, + active, + onSelect, + query, + density = 'comfortable', + onDelete, + onReprocess, +}: Props) { + const compact = density === 'compact' + const vpad = compact ? 10 : 14 + const apiStatus = STATUS_MAP[t.status] ?? 'idle' + const statusIcon = statusIconFor(apiStatus) + const sourceLabel = t.source === 'room' ? t.room || 'room' : t.source + const isError = apiStatus === 'error' + const errorMsg = isError ? t.error_message || t.error || 'Processing failed — reason unavailable' : null + const snippet = query && t.snippet ? t.snippet : null + const matchCount = query && t.snippet ? 1 : 0 + + const [srcLang, tgtLang] = (t.lang || '').includes('→') + ? (t.lang as string).split('→').map((s) => s.trim()) + : [t.lang, null] + + return ( +
onSelect?.(t.id)} + style={{ + display: 'grid', + gridTemplateColumns: 'auto 1fr auto auto', + alignItems: 'center', + columnGap: 14, + padding: `${vpad}px 20px`, + borderBottom: '1px solid var(--border)', + cursor: 'pointer', + position: 'relative', + }} + > + {active && ( + + )} + + {statusIcon.node} + +
+ + + + +
+ {sourceLabel} + · + {fmtDate(t.date)} + · + {fmtDur(t.duration)} + + {t.speakers > 0 && ( + <> + · + + {I.Users(11)} {t.speakers} {t.speakers === 1 ? 'speaker' : 'speakers'} + + + )} + + {srcLang && ( + <> + · + + {I.Globe(11)} + + {srcLang} + {tgtLang && <> → {tgtLang}} + + + + )} +
+ + {errorMsg && ( +
+ {I.AlertTriangle(11)} + {errorMsg} +
+ )} + + {snippet && ( +
+ “” +
+ )} +
+ + + {matchCount > 0 && ( + + {matchCount} match + + )} + + + +
+ ) +} diff --git a/ui/src/components/browse/TrashRow.tsx b/ui/src/components/browse/TrashRow.tsx new file mode 100644 index 00000000..25ae025a --- /dev/null +++ b/ui/src/components/browse/TrashRow.tsx @@ -0,0 +1,101 @@ +import { I } from '@/components/icons' +import { RowMenuTrigger } from '@/components/ui/primitives' +import { fmtDate, fmtDur } from '@/lib/format' +import type { TranscriptRowData } from '@/lib/types' + +type Props = { + t: TranscriptRowData + onRestore?: (id: string) => void + onDestroy?: (t: TranscriptRowData) => void +} + +export function TrashRow({ t, onRestore, onDestroy }: Props) { + const sourceLabel = t.source === 'room' ? t.room || 'room' : t.source + return ( +
+ {I.Trash(14)} + +
+ + {t.title || 'Unnamed transcript'} + + +
+ {sourceLabel} + · + {fmtDate(t.date)} + {t.duration > 0 && ( + <> + · + + {fmtDur(t.duration)} + + + )} + · + + {I.Trash(11)} Deleted + +
+
+ + onRestore?.(t.id), + }, + { separator: true }, + { + label: 'Destroy permanently', + icon: I.Trash(14), + danger: true, + onClick: () => onDestroy?.(t), + }, + ]} + /> +
+ ) +} diff --git a/ui/src/components/home/LanguagePair.tsx b/ui/src/components/home/LanguagePair.tsx new file mode 100644 index 00000000..84ff8cc6 --- /dev/null +++ b/ui/src/components/home/LanguagePair.tsx @@ -0,0 +1,97 @@ +import { I } from '@/components/icons' +import { REFLECTOR_LANGS } from '@/lib/types' + +type Props = { + sourceLang: string + setSourceLang: (v: string) => void + targetLang: string + setTargetLang: (v: string) => void + horizontal?: boolean +} + +export function LanguagePair({ + sourceLang, + setSourceLang, + targetLang, + setTargetLang, + horizontal, +}: Props) { + return ( +
+
+ + +
Detected from the audio if set to Auto.
+
+ + {horizontal && ( + + )} + +
+ + +
Leave blank to skip translation.
+
+
+ ) +} diff --git a/ui/src/components/home/RoomPicker.tsx b/ui/src/components/home/RoomPicker.tsx new file mode 100644 index 00000000..4f77066a --- /dev/null +++ b/ui/src/components/home/RoomPicker.tsx @@ -0,0 +1,33 @@ +import { I } from '@/components/icons' +import type { RoomRowData } from '@/lib/types' + +type Props = { + roomId: string + setRoomId: (v: string) => void + rooms: RoomRowData[] +} + +export function RoomPicker({ roomId, setRoomId, rooms }: Props) { + return ( +
+ + +
+ ) +} diff --git a/ui/src/components/icons.tsx b/ui/src/components/icons.tsx new file mode 100644 index 00000000..b7792b08 --- /dev/null +++ b/ui/src/components/icons.tsx @@ -0,0 +1,162 @@ +import { + AlertTriangle, + ArrowRight, + ArrowUpDown, + Bell, + Calendar, + Check, + CheckCircle2, + ChevronDown, + ChevronLeft, + ChevronRight, + Clock, + Cloud, + Copy, + DoorClosed, + DoorOpen, + Download, + Edit, + ExternalLink, + File, + FileAudio, + Filter, + Folder, + Globe, + History, + Inbox, + Info, + Link as LinkIcon, + Loader, + Lock, + Mic, + Moon, + MoreHorizontal, + Plus, + Radio, + RefreshCw, + RotateCcw, + Search, + Settings, + Share2, + Shield, + Sparkles, + Sun, + Tag, + Trash2, + Undo, + Upload, + User, + Users, + Waves, + X, +} from 'lucide-react' + +export { + AlertTriangle, + ArrowRight, + Bell, + Calendar, + Check, + CheckCircle2, + ChevronDown, + ChevronLeft, + ChevronRight, + Clock, + Cloud, + Copy, + Download, + Edit, + ExternalLink, + File, + FileAudio, + Filter, + Folder, + Globe, + History, + Inbox, + Info, + Loader, + Lock, + Mic, + Moon, + Plus, + Radio, + RefreshCw, + RotateCcw, + Search, + Settings, + Shield, + Sparkles, + Sun, + Tag, + Undo, + Upload, + User, + Users, + Waves, + X, +} +export { DoorClosed as Door } +export { DoorOpen } +export { Trash2 as Trash } +export { MoreHorizontal as More } +export { Share2 as Share } +export { ArrowUpDown as Swap } +export { LinkIcon as Link } +export { X as Close } + +const make = (Icon: typeof Mic) => (size = 16) => + +export const I = { + Inbox: make(Inbox), + Mic: make(Mic), + Upload: make(Upload), + Radio: make(Radio), + Door: make(DoorClosed), + Folder: make(Folder), + Trash: make(Trash2), + Tag: make(Tag), + Users: make(Users), + Search: make(Search), + Plus: make(Plus), + Bell: make(Bell), + Settings: make(Settings), + Close: make(X), + Download: make(Download), + Share: make(Share2), + More: make(MoreHorizontal), + Globe: make(Globe), + Clock: make(Clock), + CheckCircle: make(CheckCircle2), + AlertTriangle: make(AlertTriangle), + Loader: make(Loader), + ChevronDown: make(ChevronDown), + ChevronLeft: make(ChevronLeft), + ChevronRight: make(ChevronRight), + Sparkle: make(Sparkles), + Waves: make(Waves), + Filter: make(Filter), + Undo: make(Undo), + Edit: make(Edit), + Refresh: make(RefreshCw), + ExternalLink: make(ExternalLink), + RotateCcw: make(RotateCcw), + X: make(X), + Info: make(Info), + Check: make(Check), + Moon: make(Moon), + Sun: make(Sun), + Lock: make(Lock), + Shield: make(Shield), + Swap: make(ArrowUpDown), + ArrowRight: make(ArrowRight), + History: make(History), + DoorOpen: make(DoorOpen), + FileAudio: make(FileAudio), + File: make(File), + Calendar: make(Calendar), + Link: make(LinkIcon), + Cloud: make(Cloud), + User: make(User), + Copy: make(Copy), +} diff --git a/ui/src/components/layout/AppShell.tsx b/ui/src/components/layout/AppShell.tsx new file mode 100644 index 00000000..81b2f03b --- /dev/null +++ b/ui/src/components/layout/AppShell.tsx @@ -0,0 +1,37 @@ +import { type ReactNode } from 'react' +import { TopBar } from './TopBar' + +type AppShellProps = { + title: string + crumb?: string[] + sidebar?: ReactNode + children: ReactNode +} + +export function AppShell({ title, crumb, sidebar, children }: AppShellProps) { + return ( +
+ {sidebar} +
+ +
+ {children} +
+
+
+ ) +} diff --git a/ui/src/components/layout/AppSidebar.tsx b/ui/src/components/layout/AppSidebar.tsx new file mode 100644 index 00000000..2f627e83 --- /dev/null +++ b/ui/src/components/layout/AppSidebar.tsx @@ -0,0 +1,331 @@ +import type { CSSProperties } from 'react' +import { I } from '@/components/icons' +import { Button, SectionLabel, SidebarItem } from '@/components/ui/primitives' +import type { RoomRowData, SidebarFilter, TagRowData } from '@/lib/types' +import { BrandHeader, PrimaryNav, UserChip, sidebarAsideStyle } from './sidebarChrome' +import { useAuth } from '@/auth/AuthContext' + +type AppSidebarProps = { + filter: SidebarFilter + onFilter: (filter: SidebarFilter) => void + rooms: RoomRowData[] + tags: TagRowData[] + showTags?: boolean + collapsed: boolean + onToggle: () => void + onNewRecording?: () => void + counts?: { + all?: number | null + liveTranscripts?: number | null + uploadedFiles?: number | null + trash?: number | null + } +} + +export function AppSidebar({ + filter, + onFilter, + rooms, + tags, + showTags = true, + collapsed, + onToggle, + onNewRecording, + counts, +}: AppSidebarProps) { + const { user } = useAuth() + const myRooms = rooms.filter((r) => !r.shared) + const sharedRooms = rooms.filter((r) => r.shared) + + return ( + + ) +} + +type ExpandedNavProps = { + filter: SidebarFilter + onFilter: (filter: SidebarFilter) => void + myRooms: RoomRowData[] + sharedRooms: RoomRowData[] + tags: TagRowData[] + showTags?: boolean + onNewRecording?: () => void + counts?: AppSidebarProps['counts'] +} + +function ExpandedNav({ + filter, + onFilter, + myRooms, + sharedRooms, + tags, + showTags = true, + onNewRecording, + counts, +}: ExpandedNavProps) { + const isActive = (kind: SidebarFilter['kind'], val: SidebarFilter['value'] = null) => + filter.kind === kind && filter.value === val + + return ( + <> +
+ +
+ + + + ) +} + +type CollapsedRailProps = { + filter: SidebarFilter + onFilter: (filter: SidebarFilter) => void + onToggle: () => void + onNewRecording?: () => void +} + +function CollapsedRail({ filter, onFilter, onToggle, onNewRecording }: CollapsedRailProps) { + const items: Array<{ + kind: SidebarFilter['kind'] + value?: SidebarFilter['value'] + icon: ReturnType + title: string + }> = [ + { kind: 'all', icon: I.Inbox(18), title: 'All' }, + { kind: 'recent', icon: I.Sparkle(18), title: 'Recent' }, + { kind: 'source', value: 'live', icon: I.Radio(18), title: 'Live' }, + { kind: 'source', value: 'file', icon: I.Upload(18), title: 'Uploads' }, + { kind: 'trash', icon: I.Trash(18), title: 'Trash' }, + ] + return ( + + ) +} diff --git a/ui/src/components/layout/ReflectorMark.tsx b/ui/src/components/layout/ReflectorMark.tsx new file mode 100644 index 00000000..599e2f12 --- /dev/null +++ b/ui/src/components/layout/ReflectorMark.tsx @@ -0,0 +1,22 @@ +export function ReflectorMark({ size = 28 }: { size?: number }) { + return ( + + ) +} diff --git a/ui/src/components/layout/RoomsSidebar.tsx b/ui/src/components/layout/RoomsSidebar.tsx new file mode 100644 index 00000000..47cec88d --- /dev/null +++ b/ui/src/components/layout/RoomsSidebar.tsx @@ -0,0 +1,313 @@ +import type { CSSProperties } from 'react' +import type { components } from '@/api/schema' +import { I } from '@/components/icons' +import { Button, SectionLabel, SidebarItem } from '@/components/ui/primitives' +import type { RoomsFilter } from '@/lib/types' +import { BrandHeader, PrimaryNav, UserChip, sidebarAsideStyle } from './sidebarChrome' +import { useAuth } from '@/auth/AuthContext' + +type Room = components['schemas']['RoomDetails'] + +type Props = { + filter: RoomsFilter + onFilter: (f: RoomsFilter) => void + rooms: Room[] + collapsed: boolean + onToggle: () => void + onNewRecording?: () => void +} + +const PLATFORM_COLOR: Record = { + whereby: 'var(--status-processing)', + daily: 'var(--status-ok)', + livekit: 'var(--primary)', +} + +const PLATFORMS: Room['platform'][] = ['whereby', 'daily', 'livekit'] + +export function RoomsSidebar({ + filter, + onFilter, + rooms, + collapsed, + onToggle, + onNewRecording, +}: Props) { + const { user } = useAuth() + const isActive = ( + kind: RoomsFilter['kind'], + val: RoomsFilter['value'] | null = null, + ) => filter.kind === kind && (filter.value ?? null) === val + + const counts = { + all: rooms.length, + mine: rooms.filter((r) => !r.is_shared).length, + shared: rooms.filter((r) => r.is_shared).length, + calendar: rooms.filter((r) => r.ics_enabled).length, + } + + const platformCount = (p: Room['platform']) => + rooms.filter((r) => r.platform === p).length + const sizeCount = (s: string) => rooms.filter((r) => r.room_mode === s).length + const recCount = (t: string) => rooms.filter((r) => r.recording_type === t).length + + const presentPlatforms = PLATFORMS.filter((p) => platformCount(p) > 0) + + return ( + + ) +} + +type RailProps = { + filter: RoomsFilter + onFilter: (f: RoomsFilter) => void + onToggle: () => void + onNewRecording?: () => void +} + +function RoomsRail({ filter, onFilter, onToggle, onNewRecording }: RailProps) { + const items: Array<{ + kind: RoomsFilter['kind'] + value: RoomsFilter['value'] | null + icon: ReturnType + title: string + }> = [ + { kind: 'all', value: null, icon: I.Door(18), title: 'All rooms' }, + { kind: 'scope', value: 'mine', icon: I.User(18), title: 'My rooms' }, + { kind: 'scope', value: 'shared', icon: I.Share(18), title: 'Shared' }, + { kind: 'status', value: 'active', icon: I.Radio(18), title: 'Active' }, + { kind: 'status', value: 'calendar', icon: I.Calendar(18), title: 'Calendar' }, + ] + return ( + + ) +} diff --git a/ui/src/components/layout/TopBar.tsx b/ui/src/components/layout/TopBar.tsx new file mode 100644 index 00000000..52cc0528 --- /dev/null +++ b/ui/src/components/layout/TopBar.tsx @@ -0,0 +1,99 @@ +import { Fragment } from 'react' +import { I } from '@/components/icons' +import { Button } from '@/components/ui/primitives' + +type TopBarProps = { + title: string + crumb?: string[] +} + +export function TopBar({ title, crumb }: TopBarProps) { + return ( +
+
+ {crumb && crumb.length > 0 && ( +
+ {crumb.map((c, i) => ( + + + {c} + + {i < crumb.length - 1 && ( + / + )} + + ))} +
+ )} +
+

+ {title} +

+
+
+ +
+ + +
+ ) +} diff --git a/ui/src/components/layout/sidebarChrome.tsx b/ui/src/components/layout/sidebarChrome.tsx new file mode 100644 index 00000000..a7a44acf --- /dev/null +++ b/ui/src/components/layout/sidebarChrome.tsx @@ -0,0 +1,330 @@ +import { useEffect, useRef, useState } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' +import { I } from '@/components/icons' +import { SidebarItem } from '@/components/ui/primitives' +import { useAuth } from '@/auth/AuthContext' +import { ReflectorMark } from './ReflectorMark' + +/** + * Top-level nav shared by AppSidebar and RoomsSidebar — sits above the + * filter/context sections, below the New Recording button. + */ +export function PrimaryNav() { + const navigate = useNavigate() + const location = useLocation() + const onTranscripts = + location.pathname === '/' || + location.pathname.startsWith('/browse') || + location.pathname.startsWith('/transcripts') || + location.pathname.startsWith('/transcript/') + const onRooms = location.pathname.startsWith('/rooms') + return ( +
+ navigate('/browse')} + /> + navigate('/rooms')} + /> +
+ ) +} + +export function BrandHeader({ + collapsed, + onToggle, +}: { + collapsed: boolean + onToggle: () => void +}) { + return ( +
+ {collapsed ? ( + + ) : ( + <> +
+ +
+ + Reflector + + + by Greyhaven + +
+
+ + + )} +
+ ) +} + +export function UserChip({ + user, +}: { + user: { name?: string | null; email?: string | null } | null | undefined +}) { + const { logout } = useAuth() + const [open, setOpen] = useState(false) + const wrapperRef = useRef(null) + const displayName = user?.name || user?.email || 'Signed in' + + useEffect(() => { + if (!open) return + const onDown = (e: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { + setOpen(false) + } + } + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOpen(false) + } + document.addEventListener('mousedown', onDown) + document.addEventListener('keydown', onKey) + return () => { + document.removeEventListener('mousedown', onDown) + document.removeEventListener('keydown', onKey) + } + }, [open]) + + return ( +
+ {open && ( +
+ setOpen(false)} + disabled + /> +
+ { + setOpen(false) + void logout() + }} + /> +
+ )} + +
+ ) +} + +function MenuRow({ + icon, + label, + onClick, + danger, + disabled, +}: { + icon: React.ReactNode + label: string + onClick: () => void + danger?: boolean + disabled?: boolean +}) { + return ( + + ) +} + +function initials(name: string) { + return ( + name + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((p) => p[0]?.toUpperCase() ?? '') + .join('') || 'R' + ) +} + +export const sidebarAsideStyle = (collapsed: boolean) => + ({ + width: collapsed ? 64 : 252, + transition: 'width var(--dur-normal) var(--ease-default)', + background: 'var(--secondary)', + borderRight: '1px solid var(--border)', + display: 'flex', + flexDirection: 'column', + flexShrink: 0, + fontFamily: 'var(--font-sans)', + }) as const diff --git a/ui/src/components/rooms/DeleteRoomDialog.tsx b/ui/src/components/rooms/DeleteRoomDialog.tsx new file mode 100644 index 00000000..a9ca62db --- /dev/null +++ b/ui/src/components/rooms/DeleteRoomDialog.tsx @@ -0,0 +1,111 @@ +import { useEffect } from 'react' +import { I } from '@/components/icons' +import { Button } from '@/components/ui/primitives' + +type Props = { + name: string + onClose: () => void + onConfirm: () => void + loading?: boolean +} + +export function DeleteRoomDialog({ name, onClose, onConfirm, loading }: Props) { + useEffect(() => { + const k = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', k) + return () => document.removeEventListener('keydown', k) + }, [onClose]) + + return ( + <> +
+
+
+
+
+ {I.Trash(18)} +
+
+

+ Delete room? +

+

+ + /{name} + {' '} + will be permanently removed. Existing recordings from this room are not affected. + This can't be undone. +

+
+
+
+
+ + +
+
+ + ) +} diff --git a/ui/src/components/rooms/RoomFormDialog.tsx b/ui/src/components/rooms/RoomFormDialog.tsx new file mode 100644 index 00000000..4550addc --- /dev/null +++ b/ui/src/components/rooms/RoomFormDialog.tsx @@ -0,0 +1,834 @@ +import { useEffect, useState, type CSSProperties, type ReactNode } from 'react' +import { useQuery } from '@tanstack/react-query' +import type { components } from '@/api/schema' +import { apiClient } from '@/api/client' +import { I } from '@/components/icons' +import { Button } from '@/components/ui/primitives' +import { Combobox } from '@/components/ui/Combobox' + +type Room = components['schemas']['RoomDetails'] + +export type RoomFormPayload = { + name: string + platform: 'whereby' | 'daily' | 'livekit' + room_mode: string + recording_type: string + recording_trigger: string + is_locked: boolean + is_shared: boolean + skip_consent: boolean + store_video: boolean + zulip_auto_post: boolean + zulip_stream: string + zulip_topic: string + webhook_url: string + webhook_secret: string + ics_url: string | null + ics_enabled: boolean + ics_fetch_interval: number + email_transcript_to: string | null +} + +type Props = { + room: Room | null + onClose: () => void + onSave: (payload: RoomFormPayload) => Promise + saving?: boolean +} + +const NAME_RE = /^[a-z0-9-_]+$/i + +const TABS = [ + { id: 'general', label: 'General' }, + { id: 'calendar', label: 'Calendar' }, + { id: 'share', label: 'Share' }, + { id: 'webhook', label: 'WebHook' }, +] as const + +type TabId = (typeof TABS)[number]['id'] + +export function RoomFormDialog({ room, onClose, onSave, saving }: Props) { + const isEdit = !!room + const [tab, setTab] = useState('general') + + const [name, setName] = useState(room?.name ?? '') + const [platform, setPlatform] = useState(room?.platform ?? 'whereby') + const [roomMode, setRoomMode] = useState(room?.room_mode ?? 'normal') + const [recType, setRecType] = useState(room?.recording_type ?? 'cloud') + const [recTrigger, setRecTrigger] = useState( + room?.recording_trigger ?? 'automatic-2nd-participant', + ) + const [isLocked, setIsLocked] = useState(room?.is_locked ?? false) + const [isShared, setIsShared] = useState(room?.is_shared ?? false) + const [skipConsent, setSkipConsent] = useState(room?.skip_consent ?? false) + const [storeVideo, setStoreVideo] = useState(room?.store_video ?? false) + + const [icsEnabled, setIcsEnabled] = useState(room?.ics_enabled ?? false) + const [icsUrl, setIcsUrl] = useState(room?.ics_url ?? '') + const [icsFetchInterval, setIcsFetchInterval] = useState(room?.ics_fetch_interval ?? 5) + + const [zulipAutoPost, setZulipAutoPost] = useState(room?.zulip_auto_post ?? false) + const [zulipStream, setZulipStream] = useState(room?.zulip_stream ?? '') + const [zulipTopic, setZulipTopic] = useState(room?.zulip_topic ?? '') + + const [webhookUrl, setWebhookUrl] = useState(room?.webhook_url ?? '') + const [webhookSecret, setWebhookSecret] = useState(room?.webhook_secret ?? '') + + const [emailTranscriptTo, setEmailTranscriptTo] = useState(room?.email_transcript_to ?? '') + + const [formError, setFormError] = useState(null) + + const configQuery = useQuery({ + queryKey: ['config'], + queryFn: async () => { + const { data, response } = await apiClient.GET('/v1/config') + if (!response.ok || !data) throw new Error('Config unavailable') + return data + }, + staleTime: 5 * 60_000, + }) + + const zulipEnabled = configQuery.data?.zulip_enabled ?? false + const emailEnabled = configQuery.data?.email_enabled ?? false + + const visibleTabs = TABS.filter((t) => t.id !== 'share' || zulipEnabled) + useEffect(() => { + if (!visibleTabs.some((t) => t.id === tab)) setTab('general') + }, [visibleTabs, tab]) + + useEffect(() => { + const k = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !saving) onClose() + } + document.addEventListener('keydown', k) + return () => document.removeEventListener('keydown', k) + }, [onClose, saving]) + + const nameError = + !isEdit && name && !NAME_RE.test(name) + ? 'No spaces or special characters allowed' + : '' + const canSave = name.trim().length > 0 && !nameError && !saving + + const submit = async () => { + setFormError(null) + if (!canSave) return + try { + const effectivePlatform = platform + const effectiveRoomMode = effectivePlatform === 'daily' ? 'group' : roomMode + const effectiveTrigger = + effectivePlatform === 'daily' + ? recType === 'cloud' + ? 'automatic-2nd-participant' + : 'none' + : recTrigger + await onSave({ + name, + platform: effectivePlatform, + room_mode: effectiveRoomMode, + recording_type: recType, + recording_trigger: effectiveTrigger, + is_locked: isLocked, + is_shared: isShared, + skip_consent: skipConsent, + store_video: storeVideo, + zulip_auto_post: zulipAutoPost, + zulip_stream: zulipStream, + zulip_topic: zulipTopic, + webhook_url: webhookUrl, + webhook_secret: webhookSecret, + ics_url: icsUrl || null, + ics_enabled: icsEnabled, + ics_fetch_interval: icsFetchInterval, + email_transcript_to: emailTranscriptTo || null, + }) + } catch (err) { + setFormError(err instanceof Error ? err.message : 'Save failed') + } + } + + const panelStyle: CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 16, + padding: 20, + overflow: 'auto', + flex: 1, + maxHeight: 'calc(100vh - 260px)', + } + + return ( + <> +
!saving && onClose()} /> +
+
{ + e.preventDefault() + void submit() + }} + style={{ display: 'flex', flexDirection: 'column' }} + > +
+
+

+ {isEdit ? 'Edit room' : 'New room'} +

+ {isEdit && ( +

+ /{room!.name} +

+ )} +
+ +
+ +
+ {visibleTabs.map((t) => ( + + ))} +
+ + {formError && ( +
+ {formError} +
+ )} + +
+ {tab === 'general' && ( + + )} + {tab === 'calendar' && ( + + )} + {tab === 'share' && ( + + )} + {tab === 'webhook' && ( + + )} +
+ +
+
+ + +
+
+
+ + ) +} + +/* ---------- Field primitives ---------- */ + +function FormField({ + label, + hint, + info, + children, +}: { + label: ReactNode + hint?: ReactNode + info?: string + children: ReactNode +}) { + return ( +
+ +
{children}
+ {hint &&
{hint}
} +
+ ) +} + +function Checkbox({ + checked, + onChange, + label, + hint, +}: { + checked: boolean + onChange: (v: boolean) => void + label: ReactNode + hint?: ReactNode +}) { + return ( + + ) +} + +function InfoBanner({ children }: { children: ReactNode }) { + return ( +
+ + {I.Info(14)} + +
{children}
+
+ ) +} + +/* ---------- Tabs ---------- */ + +type GeneralTabProps = { + name: string + setName: (v: string) => void + nameError: string + isEdit: boolean + platform: Room['platform'] + setPlatform: (v: Room['platform']) => void + isLocked: boolean + setIsLocked: (v: boolean) => void + roomMode: string + setRoomMode: (v: string) => void + recType: string + setRecType: (v: string) => void + recTrigger: string + setRecTrigger: (v: string) => void + isShared: boolean + setIsShared: (v: boolean) => void + skipConsent: boolean + setSkipConsent: (v: boolean) => void + storeVideo: boolean + setStoreVideo: (v: boolean) => void + emailEnabled: boolean + emailTranscriptTo: string + setEmailTranscriptTo: (v: string) => void +} + +function GeneralTab(p: GeneralTabProps) { + const isDaily = p.platform === 'daily' + return ( + <> + + p.setName(e.target.value)} + style={p.nameError ? { borderColor: 'var(--destructive)' } : undefined} + /> + {p.isEdit && ( +
+ Room name can't be changed after creation. +
+ )} +
+ + + + + + + + {!isDaily && ( + + + + )} + + + + + + {p.recType !== 'none' && !isDaily && ( + + + + )} + + + + + + {p.emailEnabled && ( + + p.setEmailTranscriptTo(e.target.value)} + /> + + )} + + ) +} + +type CalendarTabProps = { + icsEnabled: boolean + setIcsEnabled: (v: boolean) => void + icsUrl: string + setIcsUrl: (v: string) => void + icsFetchInterval: number + setIcsFetchInterval: (v: number) => void +} + +function CalendarTab(p: CalendarTabProps) { + return ( + <> + + Reflector polls the calendar on the configured interval. Meeting titles from the feed + replace the generic "Meeting" label on recordings. + + + + + {p.icsEnabled && ( + <> + + p.setIcsUrl(e.target.value)} + /> + + + + p.setIcsFetchInterval(Math.max(1, Number(e.target.value) || 1))} + /> + + + )} + + ) +} + +type ShareTabProps = { + zulipEnabled: boolean + zulipAutoPost: boolean + setZulipAutoPost: (v: boolean) => void + zulipStream: string + setZulipStream: (v: string) => void + zulipTopic: string + setZulipTopic: (v: string) => void +} + +function ShareTab(p: ShareTabProps) { + const { data: streams = [] } = useQuery({ + queryKey: ['zulip', 'streams'], + queryFn: async () => { + const { data, response } = await apiClient.GET('/v1/zulip/streams') + if (!response.ok || !data) throw new Error('Failed to load Zulip streams') + return data + }, + enabled: p.zulipEnabled, + staleTime: 5 * 60_000, + }) + const selectedStreamId = + streams.find((s) => s.name === p.zulipStream)?.stream_id ?? null + const { data: topics = [] } = useQuery({ + queryKey: ['zulip', 'topics', selectedStreamId], + queryFn: async () => { + if (selectedStreamId == null) return [] + const { data, response } = await apiClient.GET( + '/v1/zulip/streams/{stream_id}/topics', + { params: { path: { stream_id: selectedStreamId } } }, + ) + if (!response.ok || !data) throw new Error('Failed to load Zulip topics') + return data + }, + enabled: p.zulipEnabled && selectedStreamId != null, + staleTime: 60_000, + }) + + if (!p.zulipEnabled) { + return ( + + Zulip integration isn't configured on this Reflector instance. Set ZULIP_REALM{' '} + and related env vars on the server to enable auto-posting transcript summaries. + + ) + } + return ( + <> + + Post the transcript summary + link to a Zulip channel when the meeting ends. + + + + + {p.zulipAutoPost && ( + <> + + { + p.setZulipStream(v) + p.setZulipTopic('') + }} + options={streams.map((s) => s.name)} + placeholder="e.g. reflector" + /> + + + t.name)} + placeholder="e.g. Meeting notes" + /> + + + )} + + ) +} + +type WebhookTabProps = { + webhookUrl: string + setWebhookUrl: (v: string) => void + webhookSecret: string + setWebhookSecret: (v: string) => void +} + +function WebhookTab(p: WebhookTabProps) { + return ( + <> + + Reflector POSTs a JSON payload to your URL on lifecycle events:{' '} + meeting.started,{' '} + meeting.ended,{' '} + transcript.ready. + + + + p.setWebhookUrl(e.target.value)} + /> + + + + p.setWebhookSecret(e.target.value)} + /> + + + ) +} diff --git a/ui/src/components/rooms/RoomsTable.tsx b/ui/src/components/rooms/RoomsTable.tsx new file mode 100644 index 00000000..5892e6d1 --- /dev/null +++ b/ui/src/components/rooms/RoomsTable.tsx @@ -0,0 +1,344 @@ +import { type ReactNode } from 'react' +import type { components } from '@/api/schema' +import { I } from '@/components/icons' +import { Button, RowMenuTrigger, StatusDot } from '@/components/ui/primitives' + +type Room = components['schemas']['RoomDetails'] + +type Props = { + rooms: Room[] + onEdit?: (room: Room) => void + onDelete?: (room: Room) => void + onCopy?: (room: Room) => void + copiedId?: string | null +} + +const PLATFORM_COLOR: Record = { + whereby: 'var(--status-processing)', + daily: 'var(--status-ok)', + livekit: 'var(--primary)', +} + +function platformLabel(p: Room['platform']) { + return p.charAt(0).toUpperCase() + p.slice(1) +} + +function roomUrl(room: Room) { + return `${window.location.origin}/${room.name}` +} + +function openRoom(room: Room) { + window.open(roomUrl(room), '_blank', 'noopener,noreferrer') +} + +function roomModeLabel(mode: string) { + if (mode === 'normal') return '2-4' + if (mode === 'group') return '2-200' + return mode +} + +function recordingLabel(type: string, trigger: string | null | undefined) { + if (type === 'none') return null + if (type === 'local') return 'Local recording' + if (type === 'cloud') { + if (trigger === 'automatic-2nd-participant') return 'Cloud · auto' + if (trigger === 'prompt') return 'Cloud · prompt' + return 'Cloud' + } + return type +} + +function CalendarSyncIcon({ size = 14 }: { size?: number }) { + return ( + + {I.Calendar(size)} + + {I.Refresh(size * 0.55)} + + + ) +} + +export function RoomsTable({ rooms, onEdit, onDelete, onCopy, copiedId }: Props) { + if (rooms.length === 0) return null + return ( +
+ {rooms.map((r) => ( + + ))} +
+ ) +} + +type RoomRowProps = { + room: Room + onEdit?: (room: Room) => void + onDelete?: (room: Room) => void + onCopy?: (room: Room) => void + copied?: boolean +} + +function RoomRow({ room, onEdit, onDelete, onCopy, copied }: RoomRowProps) { + const recording = recordingLabel(room.recording_type, room.recording_trigger) + return ( +
+
+ +
+ +
+ + +
+ + + {platformLabel(room.platform)} + + + + + {I.Users(11)} {roomModeLabel(room.room_mode)} + + + {recording && ( + <> + + + {room.recording_type === 'cloud' ? I.Cloud(11) : I.Download(11)} + {recording} + + + )} + + {room.zulip_auto_post && room.zulip_stream && ( + <> + + + + Z + + + {room.zulip_stream} + {room.zulip_topic && ( + <> + + {room.zulip_topic} + + )} + + + + )} +
+
+ +
+ {copied && ( + + Copied + + )} +
+ {room.ics_enabled && ( + + )} + {!copied && onCopy && ( + + )} + openRoom(room), + }, + { + label: 'Copy URL', + icon: I.Link(14), + onClick: () => onCopy?.(room), + }, + { separator: true }, + { + label: 'Edit settings', + icon: I.Edit(14), + onClick: () => onEdit?.(room), + }, + { + label: 'Delete room', + icon: I.Trash(14), + onClick: () => onDelete?.(room), + danger: true, + }, + ]} + label="Room options" + /> +
+
+
+ ) +} + +function Pill({ + icon, + title, + children, +}: { + icon?: ReactNode + title?: string + children: ReactNode +}) { + return ( + + {icon} + {children} + + ) +} + +function Dot() { + return · +} diff --git a/ui/src/components/shared/NewTranscriptDialog.tsx b/ui/src/components/shared/NewTranscriptDialog.tsx new file mode 100644 index 00000000..b30bea28 --- /dev/null +++ b/ui/src/components/shared/NewTranscriptDialog.tsx @@ -0,0 +1,237 @@ +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { toast } from 'sonner' +import { I } from '@/components/icons' +import { Button } from '@/components/ui/primitives' +import { apiClient } from '@/api/client' +import { useRooms } from '@/hooks/useRooms' +import { REFLECTOR_LANGS } from '@/lib/types' + +type Props = { + onClose: () => void +} + +export function NewTranscriptDialog({ onClose }: Props) { + const navigate = useNavigate() + const { data: rooms = [] } = useRooms() + const [title, setTitle] = useState('') + const [sourceLang, setSourceLang] = useState('auto') + const [targetLang, setTargetLang] = useState('') + const [roomId, setRoomId] = useState('') + const [submitting, setSubmitting] = useState(false) + + useEffect(() => { + const k = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !submitting) onClose() + } + document.addEventListener('keydown', k) + return () => document.removeEventListener('keydown', k) + }, [onClose, submitting]) + + const submit = async () => { + setSubmitting(true) + try { + const { data, response } = await apiClient.POST('/v1/transcripts', { + body: { + name: title || null, + source_language: sourceLang === 'auto' ? null : sourceLang, + target_language: targetLang || null, + room_id: roomId || null, + } as never, + }) + if (!response.ok || !data) throw new Error('Could not create transcript') + const id = (data as { id: string }).id + onClose() + navigate(`/browse?active=${id}`) + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to create transcript') + } finally { + setSubmitting(false) + } + } + + const handleUpload = () => { + toast.info('Upload flow lives on the transcript detail page — ship next pass.') + } + + return ( + <> +
!submitting && onClose()} /> +
+
+
+

+ New transcript +

+

+ Record live or upload a file. You can edit details later. +

+
+ +
+ +
+
+ + setTitle(e.target.value)} + style={{ marginTop: 6 }} + /> +
+ +
+ + +
Detected from the audio if set to Auto.
+
+ +
+ + +
Leave blank to skip translation.
+
+ +
+ + +
+
+ +
+
+ {I.Lock(12)} + Audio stays on your infrastructure. +
+ + +
+
+ + ) +} diff --git a/ui/src/components/transcript/AudioPlayer.tsx b/ui/src/components/transcript/AudioPlayer.tsx new file mode 100644 index 00000000..e17884e0 --- /dev/null +++ b/ui/src/components/transcript/AudioPlayer.tsx @@ -0,0 +1,219 @@ +import { useEffect, useRef, useState } from 'react' +import { I } from '@/components/icons' +import { Button } from '@/components/ui/primitives' +import { apiClient } from '@/api/client' +import { fmtDur } from '@/lib/format' +import { WaveformCanvas } from './WaveformCanvas' + +type Props = { + transcriptId: string + peaks: number[] | null | undefined + ticks?: number[] + /** Seconds. When set, the player seeks to this time. */ + seekTarget?: { seconds: number; nonce: number } | null + onTimeUpdate?: (currentSeconds: number) => void + onDuration?: (seconds: number) => void +} + +/** + * Authed audio playback for a transcript. We fetch the MP3 through the API + * client (so the Authorization header lands) and attach the blob URL to a + * native