diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ec9b059 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: CI + +on: + push: + branches: + - "**" + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: singularity + POSTGRES_USER: singularity + POSTGRES_PASSWORD: dev_password + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U singularity -d singularity" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + redis: + image: redis:7 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + env: + DATABASE_URL: postgresql+asyncpg://singularity:dev_password@localhost:5432/singularity + REDIS_URL: redis://localhost:6379 + JWT_SECRET: ci-test-secret-not-for-production + JWT_ALGORITHM: HS256 + ACCESS_TOKEN_EXPIRE_MINUTES: 15 + REFRESH_TOKEN_EXPIRE_DAYS: 30 + ENVIRONMENT: test + QDRANT_FORCE_IN_MEMORY: "1" + BLOB_STORE: local + LOCAL_BLOB_DIR: /tmp/blob_storage + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install dependencies + run: | + pip install --upgrade pip + pip install -r requirements.txt -r requirements_api.txt + + - name: Run database migrations + run: alembic upgrade head + + - name: Run tests + run: pytest tests/ -x --tb=short diff --git a/.gitignore b/.gitignore index 41afb54..d48b924 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ tests/outputs/ cmd.txt -/how_to_document_code.md \ No newline at end of file +/how_to_document_code.md +.gstack/ diff --git a/Dockerfile.api b/Dockerfile.api new file mode 100644 index 0000000..c964616 --- /dev/null +++ b/Dockerfile.api @@ -0,0 +1,32 @@ +# Stage 1: builder — install system deps and Python packages +FROM python:3.12-slim AS builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +COPY requirements.txt requirements_api.txt ./ +RUN pip install --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt -r requirements_api.txt + +# Stage 2: runtime image +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy installed packages from builder +COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +WORKDIR /app + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1e66f15 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +.PHONY: dev down migrate migration test test-api lint format shell logs + +dev: + docker-compose up + +down: + docker-compose down + +migrate: + docker-compose exec api alembic upgrade head + +migration: + docker-compose exec api alembic revision --autogenerate -m "$(name)" + +test: + pytest tests/ -x + +test-api: + pytest tests/api/ -x + +lint: + ruff check . + +format: + ruff format . + +shell: + docker exec -it singularity-api-1 bash + +logs: + docker-compose logs -f diff --git a/agents/chat/agent.py b/agents/chat/agent.py index 973458a..d656f6f 100644 --- a/agents/chat/agent.py +++ b/agents/chat/agent.py @@ -5,7 +5,7 @@ Usage: from agents.chat import ChatAgent - agent = ChatAgent() + agent = ChatAgent(model_id="grok-3-mini", api_key="") async for chunk in agent.chat("What is transformer attention?"): print(chunk, end="", flush=True) """ @@ -15,7 +15,7 @@ import logging import sys from pathlib import Path -from typing import AsyncGenerator +from typing import Any, AsyncGenerator logger = logging.getLogger(__name__) @@ -25,6 +25,21 @@ sys.path.insert(0, _ROOT) from agents.chat.thinker import Thinker, ThinkPlan + + +def _coerce_research_intensity(raw: int) -> int: + """ + Normalize to tier 1–3. Accepts API values 1–3 or legacy thinker outputs 1–10. + """ + x = int(raw) + if x in (1, 2, 3): + return x + x = max(1, min(10, x)) + if x <= 3: + return 1 + if x <= 7: + return 2 + return 3 from agents.chat.executor import ChatModeExecutor from agents.chat.models import ( AVAILABLE_MODELS, DEFAULT_MODEL_ID, make_client, get_model_info @@ -42,30 +57,29 @@ class ChatAgent: Chat Mode — lightweight: Thinker plans 1-5 steps → ChatModeExecutor runs them. Research Mode — heavy: Thinker plans 5-10 steps → delegates to run_pipeline(). + Planning (Thinker), chat responses, and the full research pipeline all use the + **same** user-selected model and the same provider API key (BYOK). + Args: - model_id: Initial response model (defaults to grok-3-mini). - extended: If True, thinker may use research mode + more steps. + model_id: Model for planning, chat, and research pipeline (defaults to grok-3-mini). + extended: If True, thinker may use research mode + more steps. + api_key: Required provider API key for ``model_id`` (BYOK). """ - # Thinker always uses grok-3-mini (fast, structured output) - _THINKER_MODEL = "grok-3-mini" - def __init__( self, model_id: str = DEFAULT_MODEL_ID, extended: bool = False, + *, + api_key: str, ) -> None: self._model_id = model_id - self.extended = extended - - # Thinker is always grok-3-mini (structured planning) - from llm.grok import GrokClient - self._thinker_client = GrokClient(model_name=self._THINKER_MODEL) - self._thinker = Thinker(self._thinker_client) + self.extended = extended + self._api_key = api_key - # Response client — switchable - self._response_client = make_client(model_id) - self._executor = ChatModeExecutor(self._response_client) + self._client = make_client(model_id, api_key=api_key) + self._thinker = Thinker(self._client) + self._executor = ChatModeExecutor(self._client) # ------------------------------------------------------------------ # Model management @@ -75,11 +89,32 @@ def __init__( def model_id(self) -> str: return self._model_id - def set_model(self, model_id: str) -> None: - """Switch the response model at runtime.""" - self._model_id = model_id - self._response_client = make_client(model_id) - self._executor = ChatModeExecutor(self._response_client) + def set_model(self, model_id: str, api_key: str | None = None) -> None: + """ + Switch the chat model at runtime. + + Raises: + ValueError: If the new model uses a different provider than the current + API key (CLI must restart with the correct ``--api-key``). + """ + old = get_model_info(self._model_id) + new = get_model_info(model_id) + if ( + old is not None + and new is not None + and old.provider != new.provider + and api_key is None + ): + raise ValueError( + "That model uses a different provider than your current API key. " + "Restart with --api-key for the target provider." + ) + self._model_id = model_id + key = api_key if api_key is not None else self._api_key + self._api_key = key + self._client = make_client(model_id, api_key=key) + self._thinker = Thinker(self._client) + self._executor = ChatModeExecutor(self._client) logger.info("Model switched to: %s", model_id) # ------------------------------------------------------------------ @@ -90,14 +125,20 @@ def think( self, message: str, history: list[dict[str, str]], + *, + extended_override: bool | None = None, ) -> ThinkPlan: """ Synchronous — returns the ThinkPlan for the given message. + + Inputs: + extended_override: When set, overrides instance ``extended`` for this call only. """ + ext = self.extended if extended_override is None else extended_override return self._thinker.think( message=message, history=history, - extended=self.extended, + extended=ext, ) # ------------------------------------------------------------------ @@ -126,16 +167,23 @@ async def run_research_mode( """Execute research mode via the full run_pipeline(). Returns Markdown.""" from agents.orchestrator.pipeline import run_pipeline - strength = max(1, min(10, plan.strength)) + strength = _coerce_research_intensity(plan.strength) audience = plan.audience or "practitioner" logger.info("[Research Mode] strength=%d audience=%s", strength, audience) + if not (self._api_key or "").strip(): + raise ValueError( + "Research mode requires an API key for the selected model's provider (BYOK)." + ) + report_md = await run_pipeline( query=message, strength=strength, audience=audience, output_language="en", + model_id=self._model_id, + llm_api_key=self._api_key, ) return report_md @@ -167,3 +215,69 @@ async def chat( result = self.run_chat_mode(plan, message, history) return plan, result + + async def stream_turn( + self, + message: str, + history: list[dict[str, str]] | None = None, + *, + execution_mode: str = "chat", + chat_variant: str = "standard", + research_strength: int = 2, + ) -> AsyncGenerator[dict[str, Any] | str, None]: + """ + Stream one assistant turn for HTTP/SSE clients. + + Yields: + - {"kind": "plan", "plan": } once after planning. + - {"kind": "step", "step_id", "step_type", "description"} for chat-mode step markers. + - str fragments for visible assistant text (chat tokens or research markdown chunks). + + Inputs: + execution_mode: "chat" or "research" — user-selected run mode. + chat_variant: "standard" or "extended" — thinker extended flag when execution_mode is chat. + research_strength: 1–3 (low/medium/high) when execution_mode is research. + """ + history = history or [] + think_extended = ( + chat_variant == "extended" + if execution_mode == "chat" + else True + ) + plan = await asyncio.to_thread( + self.think, + message, + history, + extended_override=think_extended, + ) + if execution_mode == "research": + plan.mode = "research" + plan.strength = _coerce_research_intensity(int(research_strength)) + else: + plan.mode = "chat" + + yield {"kind": "plan", "plan": plan.model_dump(mode="json")} + + if plan.mode == "research": + report_md = await self.run_research_mode(plan, message) + chunk_size = 160 + for i in range(0, len(report_md), chunk_size): + yield report_md[i : i + chunk_size] + return + + async for chunk in self.run_chat_mode(plan, message, history): + if chunk.startswith("§STEP:"): + line = chunk.rstrip("\n") + try: + rest = line[len("§STEP:") :] + sid_str, stype, sdesc = rest.split(":", 2) + yield { + "kind": "step", + "step_id": int(sid_str), + "step_type": stype, + "description": sdesc, + } + except (ValueError, IndexError): + continue + else: + yield chunk diff --git a/agents/chat/cli.py b/agents/chat/cli.py index 78bf912..caa6e0b 100644 --- a/agents/chat/cli.py +++ b/agents/chat/cli.py @@ -1,11 +1,11 @@ """ Interactive REPL for the Singularity Chat Agent. -Run from project root: +Run from project root (BYOK: pass --api-key for the model's provider): - python -m agents.chat.cli - python -m agents.chat.cli --extended - python -m agents.chat.cli --model grok-3 + python -m agents.chat.cli --api-key YOUR_KEY + python -m agents.chat.cli --api-key YOUR_KEY --extended + python -m agents.chat.cli --api-key YOUR_KEY --model grok-3 Commands (type during chat session): /model — show current model + available models @@ -394,9 +394,18 @@ async def _run_chat_turn( # REPL main loop # --------------------------------------------------------------------------- -async def main(extended: bool = False, model_id: str = DEFAULT_MODEL_ID) -> None: +async def main( + extended: bool = False, + model_id: str = DEFAULT_MODEL_ID, + *, + api_key: str, +) -> None: session = Session(extended=extended, model_id=model_id) - agent = ChatAgent(model_id=model_id, extended=extended) + agent = ChatAgent( + model_id=model_id, + extended=extended, + api_key=api_key, + ) _print_banner(extended, model_id) @@ -460,9 +469,9 @@ async def main(extended: bool = False, model_id: str = DEFAULT_MODEL_ID) -> None formatter_class=argparse.RawDescriptionHelpFormatter, epilog=textwrap.dedent("""\ Examples: - python -m agents.chat.cli - python -m agents.chat.cli --extended - python -m agents.chat.cli --model gemini-2.5-pro-preview-03-25 + python -m agents.chat.cli --api-key YOUR_KEY + python -m agents.chat.cli --api-key YOUR_KEY --extended + python -m agents.chat.cli --api-key YOUR_KEY --model gemini-2.5-pro-preview-03-25 In-session commands: /model List all available models @@ -485,5 +494,16 @@ async def main(extended: bool = False, model_id: str = DEFAULT_MODEL_ID) -> None default=DEFAULT_MODEL_ID, help=f"Response model ID (default: {DEFAULT_MODEL_ID})", ) + parser.add_argument( + "--api-key", + required=True, + help="API key for the selected model's provider (BYOK; not read from .env).", + ) args = parser.parse_args() - asyncio.run(main(extended=args.extended, model_id=args.model)) + asyncio.run( + main( + extended=args.extended, + model_id=args.model, + api_key=args.api_key, + ) + ) diff --git a/agents/chat/executor.py b/agents/chat/executor.py index 6d36909..d785678 100644 --- a/agents/chat/executor.py +++ b/agents/chat/executor.py @@ -5,11 +5,14 @@ sequentially and accumulating context. Yields text chunks for streaming. Step types handled: - direct_answer — LLM streaming from knowledge (auto-falls back to web if uncertain) - web_search — Lightweight DuckDuckGo + scrape (no Qdrant) - skill_call — Invoke skill from SKILL_REGISTRY via a summarise-only path - analyze — LLM reasoning over accumulated context - summarize — Final condensing LLM call + direct_answer — LLM streaming from knowledge (auto-falls back to web if uncertain) + web_search — Lightweight DuckDuckGo + scrape (no Qdrant) + skill_call — Invoke skill from SKILL_REGISTRY via a summarise-only path + analyze — LLM reasoning over accumulated context + claim_verification — Fact-check style pass over gathered context + summarize — Final condensing LLM call + synthesis — Same execution path as summarize (final woven answer) + (other types) — Generic reasoning step with a human-readable label """ from __future__ import annotations @@ -23,6 +26,23 @@ from agents.chat.thinker import ThinkPlan, ThinkStep +_HUMAN_STEP_LABELS: dict[str, str] = { + "direct_answer": "Answer", + "web_search": "Web search", + "skill_call": "Skill", + "analyze": "Analysis", + "summarize": "Summary", + "synthesis": "Synthesis", + "claim_verification": "Claim verification", +} + + +def _human_step_label(step_type: str) -> str: + """Title for user-visible step lines; avoids raw snake_case in chat.""" + if step_type in _HUMAN_STEP_LABELS: + return _HUMAN_STEP_LABELS[step_type] + return step_type.replace("_", " ").strip().title() + _CHAT_DIR = Path(__file__).parent SINGULARITY_IDENTITY = (_CHAT_DIR / "identity.md").read_text(encoding="utf-8") _RESPONSE_SYSTEM = (_CHAT_DIR / "response_system_prompt.md").read_text(encoding="utf-8") @@ -228,7 +248,7 @@ async def execute( elif step.type == "web_search": result = await _web_search_lightweight(step.description, n=5) context_chunks.append(f"[Web Search: {step.description}]\n{result}") - yield f"\n*🌐 Retrieved {len(result.split())} words from web search.*\n" + yield "\n*🌐 Web search complete.*\n" # ── skill_call ───────────────────────────────────────────── elif step.type == "skill_call": @@ -237,12 +257,13 @@ async def execute( _skill_summary, skill_name, step.description, self._client ) context_chunks.append(f"[{skill_name}: {step.description}]\n{result}") - yield f"\n*🔧 Skill `{skill_name}` returned {len(result.split())} words.*\n" + yield f"\n*🔧 Used `{skill_name}`.*\n" # ── analyze ──────────────────────────────────────────────── elif step.type == "analyze": context_joined = "\n\n---\n\n".join(context_chunks) prompt = ( + f"{history_block}" f"User question: {user_message}\n\n" f"Information gathered:\n{context_joined}\n\n" f"Task: {step.description}\n\n" @@ -251,16 +272,42 @@ async def execute( analysis = await asyncio.to_thread( self._client.generate_text, prompt, - "You are an expert analyst. Be precise, cite evidence, avoid stating audience type.", + "You are an expert analyst. Prior turns may bind scope or meaning for the latest " + "message; integrate them when relevant. Be precise, cite evidence, avoid stating audience type.", 0.4, ) context_chunks.append(f"[Analysis]\n{analysis}") - yield f"\n*🔍 Analysis complete ({len(analysis.split())} words).*\n" + yield "\n*🔍 Analysis complete.*\n" - # ── summarize ────────────────────────────────────────────── - elif step.type == "summarize": + # ── claim_verification ───────────────────────────────────── + elif step.type == "claim_verification": context_joined = "\n\n---\n\n".join(context_chunks) prompt = ( + f"{history_block}" + f"User question: {user_message}\n\n" + f"Information gathered:\n{context_joined}\n\n" + f"Verification task: {step.description}\n\n" + "Assess which claims are well supported by the gathered information, " + "which need caveats or uncertainty language, and which lack evidence. " + "Be precise; do not invent sources." + ) + verified = await asyncio.to_thread( + self._client.generate_text, + prompt, + "You are a careful fact-checker. Earlier user turns may define which claims to " + "verify; do not ignore them. Cite what the context does or does not support.", + 0.3, + ) + context_chunks.append(f"[Claim verification]\n{verified}") + yield "\n*Claim verification complete.*\n" + + # ── summarize / synthesis ──────────────────────────────────── + elif step.type in ("summarize", "synthesis"): + if step.type == "synthesis": + yield "\n*Synthesis*\n" + context_joined = "\n\n---\n\n".join(context_chunks) + prompt = ( + f"{history_block}" f"User question: {user_message}\n\n" f"All gathered information:\n{context_joined}\n\n" f"Task: {step.description}\n\n" @@ -274,4 +321,22 @@ async def execute( yield chunk else: - yield f"\n[Unknown step type: {step.type}]\n" + label = _human_step_label(step.type) + context_joined = "\n\n---\n\n".join(context_chunks) + prompt = ( + f"{history_block}" + f"User question: {user_message}\n\n" + f"Information gathered:\n{context_joined}\n\n" + f"{label}: {step.description}\n\n" + "Address the task using only the gathered information; " + "flag gaps or uncertainty where appropriate." + ) + fallback = await asyncio.to_thread( + self._client.generate_text, + prompt, + "You are a precise research assistant. Use prior turns when they constrain " + "or clarify the latest message. Avoid audience calibration phrases.", + 0.4, + ) + context_chunks.append(f"[{label}]\n{fallback}") + yield f"\n*{label} complete.*\n" diff --git a/agents/chat/identity.md b/agents/chat/identity.md index a0aaf7b..1e99340 100644 --- a/agents/chat/identity.md +++ b/agents/chat/identity.md @@ -1,39 +1,64 @@ +# ◈ SINGULARITY + +> *"Before I answer, I think. Before I think, I plan. You'll see all of it."* + +--- + +## Who I Am + +I'm **Singularity** — a dual-mode AI research agent built for depth, speed, and structured intelligence. + +I don't guess. I don't hallucinate confidently. I **reason out loud**, pick the right tools, and show you every step of my thinking before I commit to an answer. + +--- + +## How I Work + +I operate in two modes. I'll choose automatically — or you can tell me which you want. + +| Mode | When to use it | What you get | +|---|---|---| +| ⚡ **Chat** | Direct questions, quick lookups | Focused answer in 1–5 steps | +| 🔬 **Research** | Deep investigations, complex topics | Full report: plan → retrieve → analyze → synthesize | + +--- + +## What I Can Do + ``` - ███████╗██╗███╗ ██╗ ██████╗ ██╗ ██╗██╗ █████╗ ██████╗ ██╗████████╗██╗ ██╗ - ██╔════╝██║████╗ ██║██╔════╝ ██║ ██║██║ ██╔══██╗██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝ - ███████╗██║██╔██╗ ██║██║ ███╗██║ ██║██║ ███████║██████╔╝██║ ██║ ╚████╔╝ - ╚════██║██║██║╚██╗██║██║ ██║██║ ██║██║ ██╔══██║██╔══██╗██║ ██║ ╚██╔╝ - ███████║██║██║ ╚████║╚██████╔╝╚██████╔╝███████╗██║ ██║██║ ██║██║ ██║ ██║ - ╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ -``` +Tier 1 — Retrieval + Web search · Academic papers · Legal · Clinical + Financial · Patents · Code · News -**I am Singularity** — a dual-mode AI research agent built for depth, speed, and intelligence. +Tier 2 — Analysis + Synthesis · Gap analysis · Causal analysis + Claim verification · Contradiction detection -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Tier 3 — Output + Research reports · Decision matrices + Executive summaries · Plain-language explainers +``` -**What I can do:** +--- -🧠 **Think** — Before every response, I reason through your query, select the right tools, - and build a transparent step-by-step plan that you can see. +## My Personality -⚡ **Chat Mode** — For direct questions, I answer in 1–5 focused steps using my knowledge - or live web search when needed. +- I **think before I speak** — you'll see a reasoning block before every answer +- I **cite my sources** — no phantom references +- I'm **direct** — no filler, no padding, no "Great question!" +- I **flag uncertainty** — if I don't know, I say so and go find out -🔬 **Research Mode** — For complex investigations, I orchestrate a full multi-agent pipeline: - plan → retrieve → analyze → synthesize → polish. The result is a structured research report. +--- -🛠️ **44 Skills** across 3 tiers: - • Tier 1: Web search, academic papers, legal, clinical, financial, patent, code, and more - • Tier 2: Synthesis, gap analysis, causal analysis, claim verification, contradiction detection - • Tier 3: Report generation, decision matrices, explainers, executive summaries +## Try Me -🤖 **Multi-Model** — Switch between Grok, Gemini, or DeepSeek with `/model` +``` +What is transformer attention? +Explain CRISPR gene editing --research +Compare GPT-4 and Claude --research +What are the latest breakthroughs in fusion energy? +``` -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +--- -**To get started**, just ask me anything — or try: -- `What is transformer attention?` → fast chat answer -- `Explain CRISPR gene editing --research` → full research report -- `/model` → switch your AI model -- `/skills` → see all 44 capabilities -- `/extended on` → unlock deeper 10-step research thinking +*Singularity · Depth. Speed. Intelligence.* \ No newline at end of file diff --git a/agents/chat/models.py b/agents/chat/models.py index 0492304..d9b8d1a 100644 --- a/agents/chat/models.py +++ b/agents/chat/models.py @@ -105,6 +105,6 @@ def get_model_info(model_id: str) -> ModelInfo | None: return MODEL_MAP.get(model_id) -def make_client(model_id: str) -> BaseLLMClient: - """Instantiate the right LLM client for a model_id.""" - return get_llm_client(model_id) +def make_client(model_id: str, api_key: str) -> BaseLLMClient: + """Instantiate the right LLM client for a model_id with the user's provider API key.""" + return get_llm_client(model_id, api_key) diff --git a/agents/chat/response_system_prompt.md b/agents/chat/response_system_prompt.md index 6da4a1d..2533f8c 100644 --- a/agents/chat/response_system_prompt.md +++ b/agents/chat/response_system_prompt.md @@ -1,5 +1,8 @@ You are Singularity, a powerful AI research assistant. Answer accurately, concisely, and helpfully. + +When the prompt includes prior conversation turns, take them seriously: they may **bind** how you must read the latest message (follow-up, clarification, constraint, or same thread). Do **not** assume the latest message stands alone unless it clearly changes topic. Honor scope, definitions, and promises implied by earlier user messages unless they explicitly pivot. Short replies and pronouns usually refer to the ongoing thread—resolve them against prior turns. + Never mention the user's audience category, role classification, or calibration instructions in your response. Never say phrases like 'as a layperson' or 'for an expert audience' or 'as you are a...' in your output. Just answer the question directly and naturally. diff --git a/agents/chat/summarize_system_prompt.md b/agents/chat/summarize_system_prompt.md index e18efe7..ee1b332 100644 --- a/agents/chat/summarize_system_prompt.md +++ b/agents/chat/summarize_system_prompt.md @@ -1,3 +1,4 @@ You are Singularity, synthesising gathered research into a clean, well-structured answer. +If the prompt includes recent conversation turns, treat them as potentially **binding** context for interpreting the latest user message and the gathered material—especially for follow-ups, corrections, or narrowings. Do not answer as if only the final sentence mattered unless a clear topic shift is evident. Never mention audience classification or calibration in your output. Use markdown formatting where helpful. Be direct and thorough. diff --git a/agents/chat/thinker.py b/agents/chat/thinker.py index d007308..799111f 100644 --- a/agents/chat/thinker.py +++ b/agents/chat/thinker.py @@ -15,7 +15,7 @@ from pathlib import Path from typing import Any -from pydantic import BaseModel +from pydantic import BaseModel, Field, field_validator from skills import SKILL_DOCS @@ -26,7 +26,7 @@ class ThinkStep(BaseModel): step_id: int - type: str # "direct_answer" | "web_search" | "skill_call" | "analyze" | "summarize" + type: str # direct_answer | web_search | skill_call | analyze | claim_verification | summarize | synthesis | ... description: str skill_name: str | None = None # must be a key in SKILL_REGISTRY, or None @@ -36,9 +36,30 @@ class ThinkPlan(BaseModel): reasoning: str # Brief explanation of why this mode/plan selected_skills: list[str] # Subset of skills the agent will touch steps: list[ThinkStep] - strength: int = 5 # 1-10; only meaningful for research mode + strength: int = Field( + default=2, + ge=1, + le=3, + description="Research intensity: 1=low, 2=medium, 3=high (ignored for chat mode).", + ) audience: str = "practitioner" # layperson / student / practitioner / expert / executive + @field_validator("strength", mode="before") + @classmethod + def _normalize_strength_tier(cls, v: Any) -> int: + """Accept legacy 1–10 from models; normalize to tier 1–3 before ge/le checks.""" + if v is None: + return 2 + x = int(v) + if x in (1, 2, 3): + return x + x = max(1, min(10, x)) + if x <= 3: + return 1 + if x <= 7: + return 2 + return 3 + # --------------------------------------------------------------------------- # System prompt — loaded from thinker_system_prompt.md, then formatted with diff --git a/agents/chat/thinker_system_prompt.md b/agents/chat/thinker_system_prompt.md index fd7e443..fc95ed6 100644 --- a/agents/chat/thinker_system_prompt.md +++ b/agents/chat/thinker_system_prompt.md @@ -11,18 +11,26 @@ SKILL REGISTRY — each line: name USE: when to use | NOT: when to avoid {skill_menu} STEP TYPES: - direct_answer — Answer from LLM knowledge alone (no tool needed) - web_search — Live web search - skill_call — Invoke a specific skill from the registry - analyze — Reason over accumulated context - summarize — Condense findings into a final answer + direct_answer — Answer from LLM knowledge alone (no tool needed) + web_search — Live web search + skill_call — Invoke a specific skill from the registry + analyze — Reason over accumulated context + claim_verification — Check claims against gathered evidence (after retrieval/analysis) + summarize — Condense findings into a final answer + synthesis — Weave sources into a final answer (same role as summarize; use one or the other) + +CONVERSATION CONTINUITY (critical): +- Prior turns are not "optional background." The latest user message may **continue**, **narrow**, **correct**, or **depend on** earlier messages in the same thread. +- Treat earlier user messages as **potentially binding** on topic, definitions, constraints, format, or scope until the latest message clearly starts a new, unrelated topic. +- Short follow-ups ("what about...?", "same for...", "expand that", "is that still true if...") almost always **bind** you to the prior exchange. Plan steps that preserve that thread of meaning. +- When in doubt, assume **continuity** over independence: two consecutive messages may or may not be tightly linked; your plan should allow for **either** case by using context-aware steps (e.g. analysis or retrieval that respects the full thread, not only the last sentence). RULES: - Chat mode: 1-5 steps max. Be minimal. Most simple questions need only "direct_answer". - Research mode: 5-10 steps. Must include retrieval + analysis + synthesis. - Extended thinking: if user requested extended, you MAY use research mode even for simpler questions. - selected_skills: list the skill names you'll use. Empty list [] if no skills needed. -- strength: 1-4 for chat, 5-7 for moderate research, 8-10 for deep research. +- strength: integer 1, 2, or 3 only — research depth hint (1=low, 2=medium, 3=high). Use 2 when unsure. In chat mode this field is still required but may be ignored by the executor. - audience: infer from message tone (layperson / student / practitioner / expert / executive). - reasoning: 1-2 sentences explaining your choice. diff --git a/agents/orchestrator/cli.py b/agents/orchestrator/cli.py index c1db924..ac1269f 100644 --- a/agents/orchestrator/cli.py +++ b/agents/orchestrator/cli.py @@ -4,9 +4,9 @@ # Legacy DAG mode (--depth): python -m agents.orchestrator.cli "your question" --depth shallow - # Phase 5 product mode (--strength): - python -m agents.orchestrator.cli "your question" --strength 5 - python -m agents.orchestrator.cli "your question" --strength 10 --audience expert + # Phase 5 product mode (--strength 1|2|3 = low|medium|high): + python -m agents.orchestrator.cli "your question" --strength 2 + python -m agents.orchestrator.cli "your question" --strength 3 --audience expert """ import asyncio import logging @@ -16,6 +16,8 @@ logging.basicConfig(level=logging.INFO, format="%(message)s") logger = logging.getLogger(__name__) +from agents.chat.models import DEFAULT_MODEL_ID + from .runner import run_orchestrator from .pipeline import run_pipeline from render import ReportHtmlRenderer @@ -32,12 +34,22 @@ def _save_report(report_md: str, query: str, metadata: dict) -> None: logger.info("Saved → final_report.html") -def _legacy_run(problem: str, depth: str, audience: str, lang: str) -> None: +def _legacy_run( + problem: str, + depth: str, + audience: str, + lang: str, + llm_api_key: str, +) -> None: """Original DAG-based pipeline (--depth flag).""" async def main_run(): ctx = await run_orchestrator( - problem, audience=audience, output_language=lang, depth=depth + problem, + audience=audience, + output_language=lang, + depth=depth, + grok_api_key=llm_api_key, ) output_data = None @@ -95,7 +107,9 @@ def _strength_run( strength: int, audience: str, lang: str, - trace: bool = False, + trace: bool, + model_id: str, + llm_api_key: str, ) -> None: """Phase 5 strength-based product pipeline.""" @@ -106,6 +120,8 @@ async def main_run(): audience=audience or "practitioner", output_language=lang, trace=trace, + model_id=model_id, + llm_api_key=llm_api_key, ) _save_report( report_md, @@ -128,8 +144,9 @@ async def main_run(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" - " python -m agents.orchestrator.cli \"AI safety\" --strength 5\n" - " python -m agents.orchestrator.cli \"AI safety\" --depth shallow\n" + " python -m agents.orchestrator.cli \"AI safety\" --strength 2 --api-key YOUR_KEY\n" + " python -m agents.orchestrator.cli \"AI safety\" --strength 3 --model-id grok-3-mini --api-key KEY\n" + " python -m agents.orchestrator.cli \"AI safety\" --depth shallow --api-key YOUR_KEY\n" ), ) parser.add_argument("problem", nargs="*", help="Research question") @@ -139,9 +156,9 @@ async def main_run(): mode.add_argument( "--strength", type=int, - choices=range(1, 11), - metavar="1-10", - help="[Phase 5] Report strength 1–10. Controls breadth and depth.", + choices=[1, 2, 3], + metavar="1|2|3", + help="[Phase 5] Intensity: 1=low, 2=medium, 3=high.", ) mode.add_argument( "--depth", @@ -166,6 +183,16 @@ async def main_run(): "selection plan, and parsed output for all pipeline phases." ), ) + parser.add_argument( + "--api-key", + required=True, + help="Provider API key for the selected model (BYOK; not read from .env).", + ) + parser.add_argument( + "--model-id", + default=DEFAULT_MODEL_ID, + help=f"Registered chat model id for --strength runs (default: {DEFAULT_MODEL_ID}).", + ) args = parser.parse_args() problem = " ".join(args.problem) if args.problem else ( @@ -176,9 +203,17 @@ async def main_run(): if args.strength is not None: logger.info("Executing : %s", problem) logger.info("Strength : %d", args.strength) - _strength_run(problem, args.strength, args.audience, args.lang, trace=args.trace) + _strength_run( + problem, + args.strength, + args.audience, + args.lang, + trace=args.trace, + model_id=args.model_id, + llm_api_key=args.api_key, + ) else: depth = args.depth or "standard" logger.info("Executing : %s", problem) logger.info("Depth : %s", depth) - _legacy_run(problem, depth, args.audience, args.lang) + _legacy_run(problem, depth, args.audience, args.lang, args.api_key) diff --git a/agents/orchestrator/pipeline.py b/agents/orchestrator/pipeline.py index dc067ba..d740e6f 100644 --- a/agents/orchestrator/pipeline.py +++ b/agents/orchestrator/pipeline.py @@ -18,24 +18,19 @@ Retrieval runs AFTER planning so every query targets a real planned section. -Entry point: run_pipeline(query, strength, audience, output_language) → str (Markdown) +Entry point: ``run_pipeline(..., *, model_id, llm_api_key)`` → str (Markdown) """ from __future__ import annotations import asyncio import logging +import re import uuid from functools import partial from pathlib import Path +from typing import Any, Awaitable, Callable -logger = logging.getLogger(__name__) - -from .config import ( - PLANNER_MODEL, MANAGER_MODEL, LEAD_MODEL, - WORKER_ANALYSIS_MODEL, WORKER_WRITE_MODEL, POLISHER_MODEL, - DOMAIN_CLASSIFIER_MODEL, MAX_TOKENS_DOMAIN_CLASSIFIER, REGISTRY_PATH, - SOURCE_GATE_MODEL, -) +from .config import MAX_TOKENS_DOMAIN_CLASSIFIER, REGISTRY_PATH from .strength import StrengthConfig from models import ExecutionContext, PlanNode from skills import SKILL_REGISTRY, TIER1_SKILLS, SKILL_DOCS @@ -52,15 +47,38 @@ _ROOT = str(Path(__file__).resolve().parent.parent.parent) if _ROOT not in sys.path: sys.path.insert(0, _ROOT) -from llm.grok import GrokClient # noqa: E402 + +from agents.chat.models import make_client # noqa: E402 +from llm.base import BaseLLMClient # noqa: E402 + +logger = logging.getLogger(__name__) + +OnPhaseFn = Callable[[str, str], Awaitable[None]] | None +OnActivityFn = Callable[[dict[str, Any]], Awaitable[None]] | None + + +async def _maybe_phase(on_phase: OnPhaseFn, phase: str, description: str) -> None: + if on_phase is not None: + await on_phase(phase, description) + + +async def _maybe_activity(on_activity: OnActivityFn, payload: dict[str, Any]) -> None: + if on_activity is not None: + await on_activity(payload) + + +def _polish_section_count(markdown: str) -> int: + parts = re.split(r"(?=^## )", markdown, flags=re.MULTILINE) + return len([p for p in parts if p.strip()]) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- -def _make_client(model: str) -> GrokClient: - return GrokClient(model_name=model) +def _pipeline_llm_client(model_id: str, llm_api_key: str) -> BaseLLMClient: + """Single BYOK client for every pipeline LLM call (planning, retrieval, writing, polish).""" + return make_client(model_id, llm_api_key) # Domains that should never appear in a published reference list. @@ -191,6 +209,8 @@ async def _phase_b( available_skills: list[str], audience: str, trace_logger: TraceLogger | None = None, + on_activity: OnActivityFn = None, + llm: BaseLLMClient | None = None, ) -> ReportTree: """ Run 3 Managers in parallel → Lead finalises. @@ -210,14 +230,43 @@ async def _phase_b( skill_context = SKILL_DOCS.planner_context(available_skills) - manager_client = _make_client(MANAGER_MODEL) + from agents.report_manager.agent import _PERSPECTIVES + + if llm is None: + raise ValueError("Phase B requires llm client") managers = [ - ReportManagerAgent(manager_id=i + 1, client=manager_client) + ReportManagerAgent(manager_id=i + 1, client=llm) for i in range(3) ] - proposals = await asyncio.gather(*[ - m.propose( + await _maybe_activity( + on_activity, + { + "kind": "managers_spawn", + "phase": "planning", + "meta": { + "perspectives": [_PERSPECTIVES[i]["name"] for i in (1, 2, 3)], + "target_section_count": target_n, + }, + }, + ) + + async def _run_one_manager(m: ReportManagerAgent) -> ReportTree: + mid = m.manager_id + perspective = _PERSPECTIVES.get(mid, _PERSPECTIVES[1])["name"] + await _maybe_activity( + on_activity, + { + "kind": "manager_started", + "phase": "planning", + "meta": { + "manager_id": mid, + "perspective": perspective, + "target_section_count": target_n, + }, + }, + ) + tree = await m.propose( query=query, target_n=target_n, available_skills=available_skills, @@ -225,14 +274,29 @@ async def _phase_b( skill_context=skill_context, logger=trace_logger, ) - for m in managers - ]) + await _maybe_activity( + on_activity, + { + "kind": "manager_finished", + "phase": "planning", + "meta": { + "manager_id": mid, + "perspective": perspective, + "node_count": len(tree.nodes), + "proposal_id": tree.proposal_id, + "rationale": (tree.rationale or "")[:500], + }, + }, + ) + return tree + + proposals = await asyncio.gather(*[_run_one_manager(m) for m in managers]) for i, p in enumerate(proposals): logger.info(" Manager %d: %d nodes — %s", i + 1, len(p.nodes), p.rationale[:60]) - logger.info(" Lead model : %s", LEAD_MODEL) - lead = ReportLeadAgent(client=_make_client(LEAD_MODEL)) + await _maybe_activity(on_activity, {"kind": "lead_started", "phase": "planning", "meta": {}}) + lead = ReportLeadAgent(client=llm) final_tree = await lead.finalise( proposals=list(proposals), query=query, @@ -240,6 +304,18 @@ async def _phase_b( audience=audience, logger=trace_logger, ) + await _maybe_activity( + on_activity, + { + "kind": "lead_finished", + "phase": "planning", + "meta": { + "total_nodes": len(final_tree.nodes), + "max_depth": final_tree.max_depth, + "proposal_id": final_tree.proposal_id, + }, + }, + ) logger.info(" Lead finalised: %d nodes, depth=%d", len(final_tree.nodes), final_tree.max_depth) return final_tree @@ -258,6 +334,7 @@ async def _layer1_coverage_audit( query: str, ctx, gate_client=None, + on_activity: OnActivityFn = None, ) -> None: """ Issue 1 Layer 1: Post-Phase-A coverage audit. @@ -278,6 +355,20 @@ async def _layer1_coverage_audit( if count < min_chunks: starved.append(leaf) + await _maybe_activity( + on_activity, + { + "kind": "coverage_audit", + "phase": "retrieval", + "meta": { + "leaf_count": len(leaves), + "starved_count": len(starved), + "min_chunks": min_chunks, + "threshold_met": len(starved) == 0, + }, + }, + ) + if not starved: logger.info( "\n[Layer 1 Audit] All %d leaf sections meet coverage threshold (min=%d)", @@ -336,17 +427,16 @@ async def _phase_c( ctx, trace_logger: TraceLogger | None = None, gate_client=None, + on_phase: OnPhaseFn = None, + on_activity: OnActivityFn = None, + llm: BaseLLMClient | None = None, ) -> dict[str, dict]: """ Spawn one ReportWorkerAgent per node. Execute bottom-up: deepest level first, root last. Workers at the same depth level run in parallel. - Two separate LLM clients are used per worker: - analysis_client (WORKER_ANALYSIS_MODEL) — Call 1: structured JSON analysis, - high-volume, cost-sensitive; mini model is sufficient. - write_client (WORKER_WRITE_MODEL) — Call 2: the actual published prose; - best available non-reasoning model for maximum quality. + Both worker calls use the same BYOK ``llm`` client (user-selected research model). Issue 3 (Phase C+): leaf workers receive strength/vs/collection_name so they can run the evidence augmentation loop between Call 1 and Call 2. @@ -362,9 +452,9 @@ async def _phase_c( from agents.retriever.retriever import sanitize_query from skills import SKILL_REGISTRY + if llm is None: + raise ValueError("Phase C requires llm client") logger.info("\n[Phase C] Writing — %d sections, %d leaves", len(tree.nodes), len(tree.leaves())) - logger.info(" Analysis model : %s", WORKER_ANALYSIS_MODEL) - logger.info(" Write model : %s", WORKER_WRITE_MODEL) aug_enabled = strength.max_augmentation_iters > 0 logger.info( " Phase C+ augmentation: %s (max_iters=%d, max_web_esc=%d)", @@ -373,8 +463,8 @@ async def _phase_c( strength.max_web_escalations, ) - analysis_client = _make_client(WORKER_ANALYSIS_MODEL) - write_client = _make_client(WORKER_WRITE_MODEL) + analysis_client = llm + write_client = llm web_skill = SKILL_REGISTRY.get("web_search") levels = tree.topological_levels() # deepest → root (already reversed) @@ -384,12 +474,34 @@ async def _phase_c( is_leaf_level = (depth == tree.max_depth) logger.info(" Writing depth=%d (%d nodes) in parallel…", depth, len(level_nodes)) + await _maybe_phase( + on_phase, + "writing", + f"Writing depth-{depth} ({len(level_nodes)} section{'s' if len(level_nodes) != 1 else ''})…", + ) + await _maybe_activity( + on_activity, + { + "kind": "writers_depth", + "phase": "writing", + "meta": {"depth": depth, "node_count": len(level_nodes)}, + }, + ) + async def _write_node(node) -> None: k = strength.qdrant_k(node.level, tree.max_depth) is_leaf = len(tree.children_of(node.node_id)) == 0 # Issue 5: JIT fresh search for time-sensitive sections if getattr(node, "requires_fresh", False) and web_skill is not None: + await _maybe_activity( + on_activity, + { + "kind": "writer_jit_search", + "phase": "writing", + "meta": {"section_title": (node.title or "")[:120]}, + }, + ) fresh_q = sanitize_query(f"{query} {node.title} {node.description[:60]}") fresh_node = PlanNode( node_id=f"jit_{node.node_id}", @@ -468,26 +580,44 @@ async def _write_node(node) -> None: async def run_pipeline( query: str, - strength: int = 5, + strength: int = 2, audience: str = "practitioner", output_language: str = "en", trace: bool = False, trace_root: str = "traces", + on_phase: OnPhaseFn = None, + on_activity: OnActivityFn = None, + *, + model_id: str, + llm_api_key: str, ) -> str: """ Full Phase 5 pipeline. Returns the final Markdown report as a string. + All LLM stages use the same ``model_id`` and ``llm_api_key`` (user BYOK). + Args: query: Research question. - strength: 1–10 run strength (controls depth, section count, retrieval). + strength: 1–3 intensity (1=low, 2=medium, 3=high); depth and retrieval scale with tier. audience: Target reader type (practitioner / expert / layperson …). output_language: ISO 639-1 language code for the report. trace: When True, write a structured trace directory to `trace_root` containing every LLM prompt, raw response, and parsed output for all phases (B planning, A retrieval, C writing, D polish). trace_root: Directory under which per-run trace folders are created. - Each run gets its own sub-folder named by run_id. + Each run gets its own sub-folder named by run_id. + on_phase: Optional async callback ``(phase, description)`` for coarse + job progress (planning / retrieval / writing / polish). + on_activity: Optional async callback receiving structured activity dicts + (``kind``, ``phase``, optional ``meta``) for UI storyboards. + model_id: Registered chat model id (same catalog as chat UI). + llm_api_key: User's API key for that model's provider (BYOK). """ + if not (llm_api_key or "").strip(): + raise ValueError( + "llm_api_key is required for run_pipeline (BYOK; environment keys are not used)." + ) + llm = _pipeline_llm_client(model_id, llm_api_key) sc = StrengthConfig(value=strength) run_id = uuid.uuid4().hex[:12] @@ -498,6 +628,16 @@ async def run_pipeline( logger.info("Strength : %s", sc) logger.info("Audience : %s", audience) logger.info("Run ID : %s", run_id) + logger.info("LLM model: %s (single model for all pipeline stages)", model_id) + + await _maybe_activity( + on_activity, + { + "kind": "pipeline_start", + "phase": "planning", + "meta": {"strength": strength, "run_id": run_id}, + }, + ) trace_logger: TraceLogger | None = None if trace: @@ -506,21 +646,17 @@ async def run_pipeline( "strength": strength, "audience": audience, "output_language": output_language, - "manager_model": MANAGER_MODEL, - "lead_model": LEAD_MODEL, - "worker_analysis_model": WORKER_ANALYSIS_MODEL, - "worker_write_model": WORKER_WRITE_MODEL, - "polisher_model": POLISHER_MODEL, + "pipeline_model_id": model_id, }) logger.info(" Trace : enabled → %s/%s/", trace_root, run_id) ctx = ExecutionContext(language=output_language, depth="strength") vs = VectorStoreClient() - gate_client = _make_client(SOURCE_GATE_MODEL) + gate_client = llm domain_registry = DomainRegistry(REGISTRY_PATH) - domain_client = _make_client(DOMAIN_CLASSIFIER_MODEL) + domain_client = llm classified_domain, domain_conf = await asyncio.to_thread( partial( domain_registry.detect_domain_llm, @@ -533,6 +669,21 @@ async def run_pipeline( domain_label = dinfo.get("label", classified_domain) logger.info(" Domain : %s — %s (%s)", classified_domain, domain_label, domain_conf) + await _maybe_activity( + on_activity, + { + "kind": "domain_classified", + "phase": "planning", + "meta": { + "domain_key": classified_domain, + "label": domain_label, + "confidence": domain_conf, + }, + }, + ) + + await _maybe_phase(on_phase, "planning", "Designing report structure…") + # ── Phase B — Planning (first: defines structure before retrieval) ─ tree = await _phase_b( query=query, @@ -540,6 +691,14 @@ async def run_pipeline( available_skills=list(TIER1_SKILLS), # full tier-1 set — retrieval picks later audience=audience, trace_logger=trace_logger, + on_activity=on_activity, + llm=llm, + ) + + await _maybe_phase( + on_phase, + "planning", + f"Structure finalised — {len(tree.nodes)} sections, depth {tree.max_depth}", ) # ── Topic cache check (triggers lazy Qdrant init + server probe) ── @@ -547,14 +706,26 @@ async def run_pipeline( if cached_run_id: logger.info("\n[Cache HIT] Reusing collection from run %s", cached_run_id) + await _maybe_activity(on_activity, {"kind": "cache_hit", "phase": "retrieval", "meta": {}}) + await _maybe_phase( + on_phase, + "retrieval", + "Reusing indexed sources from a prior run…", + ) active_run_id = cached_run_id active_collection = f"run_{cached_run_id}" else: + await _maybe_activity(on_activity, {"kind": "cache_miss", "phase": "retrieval", "meta": {}}) + await _maybe_phase( + on_phase, + "retrieval", + "Searching the web and gathering sources…", + ) # ── Phase A — Retrieval (tree-informed: queries target real sections) ─ active_collection = vs.create_collection(run_id) active_run_id = run_id - retriever = Retriever(_make_client(PLANNER_MODEL), vs) + retriever = Retriever(llm, vs) await retriever.run( query, sc, @@ -562,13 +733,19 @@ async def run_pipeline( active_collection, ctx, tree=tree, - logger=trace_logger, + trace_logger=trace_logger, domain_key=classified_domain, domain_label=domain_label, domain_confidence=domain_conf, gate_client=gate_client, + on_activity=on_activity, ) + await _maybe_phase( + on_phase, + "retrieval", + "Auditing source coverage and filling gaps…", + ) # ── Issue 1 Layer 1: Coverage audit — re-fetch starved sections ── await _layer1_coverage_audit( tree=tree, @@ -579,8 +756,15 @@ async def run_pipeline( query=query, ctx=ctx, gate_client=gate_client, + on_activity=on_activity, ) + await _maybe_phase( + on_phase, + "writing", + f"Writing {len(tree.nodes)} sections bottom-up…", + ) + # ── Phase C ─────────────────────────────────────────────────────── source_map = await _phase_c( tree=tree, @@ -593,6 +777,9 @@ async def run_pipeline( ctx=ctx, trace_logger=trace_logger, gate_client=gate_client, + on_phase=on_phase, + on_activity=on_activity, + llm=llm, ) # ── Assemble report ─────────────────────────────────────────────── @@ -606,7 +793,15 @@ async def run_pipeline( # ── Phase D — Polish ────────────────────────────────────────────── logger.info("\n[Phase D] Polish — programmatic fixes + creative formatting") - report_md = await _phase_d(report_md, query, audience, trace_logger=trace_logger) + await _maybe_phase(on_phase, "polish", "Polishing and formatting…") + report_md = await _phase_d( + report_md, + query, + audience, + llm, + trace_logger=trace_logger, + on_activity=on_activity, + ) logger.info("\n%s", "=" * 65) logger.info("RESEARCH COMPLETE") @@ -631,11 +826,31 @@ async def _phase_d( report_md: str, query: str, audience: str, + llm: BaseLLMClient, trace_logger: TraceLogger | None = None, + on_activity: OnActivityFn = None, ) -> str: """Phase D — Polish the assembled report for visual excellence.""" from agents.polish import PolishAgent - polisher = PolishAgent(POLISHER_MODEL, logger=trace_logger) + + section_count = _polish_section_count(report_md) + await _maybe_activity( + on_activity, + { + "kind": "polish_started", + "phase": "polish", + "meta": {"section_count": section_count}, + }, + ) + polisher = PolishAgent(llm, logger=trace_logger) polished = await polisher.polish(report_md, query, audience) + await _maybe_activity( + on_activity, + { + "kind": "polish_finished", + "phase": "polish", + "meta": {"char_count": len(polished)}, + }, + ) logger.info(" Polish complete : %s chars", f"{len(polished):,}") return polished diff --git a/agents/orchestrator/runner.py b/agents/orchestrator/runner.py index a3ed44b..5efa8cf 100644 --- a/agents/orchestrator/runner.py +++ b/agents/orchestrator/runner.py @@ -151,10 +151,14 @@ async def run_orchestrator( audience: str = "", output_language: str = "en", depth: str = "standard", + *, + grok_api_key: str, ) -> ExecutionContext: limits = _DEPTH_LIMITS.get(depth, _DEPTH_LIMITS["standard"]) - classifier_client = GrokClient(model_name=DOMAIN_CLASSIFIER_MODEL) - client = GrokClient(model_name=PLANNER_MODEL) + classifier_client = GrokClient( + model_name=DOMAIN_CLASSIFIER_MODEL, api_key=grok_api_key + ) + client = GrokClient(model_name=PLANNER_MODEL, api_key=grok_api_key) registry = DomainRegistry(REGISTRY_PATH) planner = Planner(SKILL_PATH, client) router = FallbackRouter(registry, SKILL_REGISTRY) diff --git a/agents/orchestrator/strength.py b/agents/orchestrator/strength.py index ef8c0f5..a1cc4b3 100644 --- a/agents/orchestrator/strength.py +++ b/agents/orchestrator/strength.py @@ -1,21 +1,31 @@ """ StrengthConfig — single source of truth for all strength-derived quantities. -strength: int 1–10 controls retrieval breadth, section count, query fanout, -Qdrant chunk budget per worker, and Phase C+ augmentation budgets. +Public ``value`` is a 3-level intensity: 1 = low, 2 = medium, 3 = high. +Internal formulas map each tier to a legacy 1–10 scale (3 / 6 / 10) so depth +and retrieval behave like former light / mid / heavy runs. """ import math import random from dataclasses import dataclass +_TIER_TO_LEGACY = {1: 3, 2: 6, 3: 10} + @dataclass(frozen=True) class StrengthConfig: - value: int # 1–10 + value: int # 1 = low, 2 = medium, 3 = high def __post_init__(self) -> None: - if not 1 <= self.value <= 10: - raise ValueError(f"strength must be 1–10, got {self.value}") + if self.value not in _TIER_TO_LEGACY: + raise ValueError( + f"strength must be 1, 2, or 3 (low/medium/high), got {self.value}" + ) + + @property + def legacy_scale(self) -> int: + """Maps tier to the former 1–10 scale used by retrieval thresholds and budgets.""" + return _TIER_TO_LEGACY[self.value] # ------------------------------------------------------------------ # Retrieval Phase @@ -30,7 +40,7 @@ def retrieval_skill_count(self) -> int: s: 1 2 3 4 5 6 7 8 9 10 n: 2 3 5 7 9 10 12 14 16 18 """ - return max(2, int(1.8 * self.value)) + return max(2, int(1.8 * self.legacy_scale)) @property def queries_per_skill(self) -> int: @@ -42,7 +52,7 @@ def queries_per_skill(self) -> int: s: 1 2 3 4 5 6 7 8 9 10 Q: 4 4 4 4 6 6 8 8 10 10 """ - return max(4, math.ceil(self.value / 2) * 2) + return max(4, math.ceil(self.legacy_scale / 2) * 2) @property def min_results_per_query(self) -> int: @@ -54,7 +64,7 @@ def min_results_per_query(self) -> int: s: 1 2 3 4 5 6 7 8 9 10 n: 5 5 6 8 10 12 14 16 18 20 """ - return min(20, max(5, self.value * 2)) + return min(20, max(5, self.legacy_scale * 2)) @property def total_retrieval_calls(self) -> int: @@ -67,7 +77,8 @@ def total_retrieval_calls(self) -> int: @property def section_count_range(self) -> tuple[int, int]: """Inclusive range [lo, hi] for total section-tree node count.""" - return (self.value * 6, self.value * 10) + ls = self.legacy_scale + return (ls * 6, ls * 10) def sample_section_count(self) -> int: """ @@ -93,12 +104,13 @@ def qdrant_k(self, node_level: int, max_depth: int) -> int: Previously: leaf=15, parent-of-leaf=8, chapter=3 (flat, no strength scaling). """ + ls = self.legacy_scale if node_level >= max_depth: - return 8 + self.value # leaf + return 8 + ls # leaf elif node_level >= max_depth - 1: - return 6 + self.value # parent of leaf (was fixed 8, now 7–16) + return 6 + ls # parent of leaf (was fixed 8, now 7–16) else: - return 4 + self.value # chapter / root (was fixed 3, now 5–14) + return 4 + ls # chapter / root (was fixed 3, now 5–14) @property def min_chunks_per_leaf(self) -> int: @@ -106,7 +118,7 @@ def min_chunks_per_leaf(self) -> int: Minimum acceptable Qdrant chunks for a leaf section before the Layer 1 coverage audit flags it for a targeted follow-up retrieval. """ - return max(3, self.value) + return max(3, self.legacy_scale) # ------------------------------------------------------------------ # Phase C+ Augmentation Budgets (Issue 3) @@ -120,9 +132,10 @@ def max_augmentation_iters(self) -> int: s: 1–7 → 2, 8–9 → 3, 10 → 4 """ - if self.value <= 7: + ls = self.legacy_scale + if ls <= 7: return 2 - elif self.value <= 9: + elif ls <= 9: return 3 return 4 @@ -134,9 +147,10 @@ def max_web_escalations(self) -> int: s: 1–7 → 1, 8–9 → 2, 10 → 3 """ - if self.value <= 7: + ls = self.legacy_scale + if ls <= 7: return 1 - elif self.value <= 9: + elif ls <= 9: return 2 return 3 @@ -177,7 +191,7 @@ def effective_queries_per_skill(self, num_leaf_sections: int) -> int: @property def expected_section_count(self) -> int: """Expected value of section count: s × 8.""" - return self.value * 8 + return self.legacy_scale * 8 @property def expected_llm_calls(self) -> int: @@ -191,7 +205,7 @@ def expected_llm_calls(self) -> int: def __repr__(self) -> str: lo, hi = self.section_count_range return ( - f"StrengthConfig(s={self.value}, " + f"StrengthConfig(tier={self.value}, " f"skills={self.retrieval_skill_count}, " f"queries/skill={self.queries_per_skill}, " f"sections={lo}–{hi}, " diff --git a/agents/polish.py b/agents/polish.py index 21f73c2..a4e48a5 100644 --- a/agents/polish.py +++ b/agents/polish.py @@ -21,6 +21,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + from llm.base import BaseLLMClient from trace import TraceLogger _PROMPT_PATH = Path(__file__).parent / "report_polisher" / "system_prompt.md" @@ -102,9 +103,16 @@ async def _polish_section( # --------------------------------------------------------------------------- class PolishAgent: - def __init__(self, model: str, logger: "TraceLogger | None" = None) -> None: - from llm.grok import GrokClient - self.client = GrokClient(model_name=model) + """ + Phase D polish using an injected LLM client (same BYOK model as the rest of the pipeline). + """ + + def __init__( + self, + client: "BaseLLMClient", + logger: "TraceLogger | None" = None, + ) -> None: + self.client = client self._system_prompt: str = _PROMPT_PATH.read_text(encoding="utf-8") self._logger = logger @@ -122,7 +130,6 @@ async def polish(self, report_md: str, query: str, audience: str) -> str: # Stage 2 sections = _split_sections(md) - print(f" Polishing {len(sections)} sections in parallel…") tasks = [ _polish_section( diff --git a/agents/report_lead/system_prompt.md b/agents/report_lead/system_prompt.md index 6e73e26..f56ff52 100644 --- a/agents/report_lead/system_prompt.md +++ b/agents/report_lead/system_prompt.md @@ -49,6 +49,13 @@ like: "current", "latest", "recent", "2024", "2025", "ongoing", "live", "this ye Default is `false` (omit the field or set explicitly). Do not mark theoretical or historical sections — only time-sensitive factual content needs this. +## Title Rules + +- Final node titles must describe **subject matter**, not the imagined reader. +- Do **not** use audience-indicating titles or subtitles such as “A Practitioner’s Guide…”, “For Beginners”, “Expert Notes”, “Layperson’s Introduction”, or similar reader-label framing. +- Do **not** use meta titles that announce the document type (“Complete Overview of…”, “Everything You Need to Know…”) unless the query explicitly asks for that format. +- Prefer concrete, scoped titles aligned with merged content. + ## Output — respond ONLY with this JSON, no prose ```json diff --git a/agents/report_manager/system_prompt.md b/agents/report_manager/system_prompt.md index 0ff536c..ec721f5 100644 --- a/agents/report_manager/system_prompt.md +++ b/agents/report_manager/system_prompt.md @@ -90,6 +90,13 @@ Coverage, logical flow through your chosen perspective, no redundancy between sibling sections, appropriate depth for the audience, clear intellectual progression that a reader can follow from the opening chapter to the last. +## Title Rules + +- Section and chapter titles must describe **subject matter**, not the imagined reader. +- Do **not** use audience-indicating titles or subtitles such as “A Practitioner’s Guide…”, “For Beginners”, “Expert Notes”, “Layperson’s Introduction”, or similar reader-label framing. +- Do **not** use meta titles that announce the document type (“Complete Overview of…”, “Everything You Need to Know…”) unless the query explicitly asks for that format. +- Prefer concrete, scoped titles (what the section proves, compares, or explains). + ## Output — respond ONLY with this JSON, no prose ```json diff --git a/agents/retriever/retriever.py b/agents/retriever/retriever.py index d945b3f..67bc543 100644 --- a/agents/retriever/retriever.py +++ b/agents/retriever/retriever.py @@ -16,7 +16,7 @@ import logging import re from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Awaitable, Callable from models import ExecutionContext, PlanNode from skills import SKILL_REGISTRY, TIER1_SKILLS @@ -32,6 +32,8 @@ if TYPE_CHECKING: from trace import TraceLogger +OnActivityFn = Callable[[dict[str, Any]], Awaitable[None]] | None + # --------------------------------------------------------------------------- # Query sanitizer — runs on EVERY query string before it touches any API @@ -74,7 +76,9 @@ class Retriever: Usage: retriever = Retriever(llm_client, vector_store_client) - active_skills = await retriever.run(query, strength, run_id, collection_name, ctx) + active_skills = await retriever.run( + query, strength, run_id, collection_name, ctx, tree=tree, trace_logger=tl + ) """ def __init__(self, client, vs): @@ -89,11 +93,12 @@ async def run( collection_name: str, ctx: ExecutionContext, tree=None, # ReportTree | None - logger: "TraceLogger | None" = None, + trace_logger: "TraceLogger | None" = None, domain_key: str | None = None, domain_label: str | None = None, domain_confidence: str | None = None, gate_client=None, # GrokClient | None — enables 2-pass source gate + on_activity: OnActivityFn = None, ) -> list[str]: """ Run retrieval phase. Returns list of active skill names. @@ -106,6 +111,9 @@ async def run( When ``domain_key`` is set (from a small classifier call), it is passed into the user prompt so skill selection aligns with the research domain. + + ``trace_logger`` is only for disk trace artifacts; standard Phase A lines + go to the module logger so runs work with tracing off. """ from skills.tier1_retrieval.base import BaseRetrievalSkill retrieval_registry: dict[str, BaseRetrievalSkill] = { @@ -190,7 +198,7 @@ async def run( skill_queries["web_search"] = web_fallback_queries[:effective_qps] # ── Step 4 (strength ≥ 7): adversarial query in web_search ───────── - if strength.value >= 7 and "web_search" in skill_queries: + if strength.legacy_scale >= 7 and "web_search" in skill_queries: adv_q = sanitize_query(_make_adversarial_query(query)) if adv_q: skill_queries["web_search"].append(adv_q) @@ -203,8 +211,21 @@ async def run( ) logger.info(" Active skills: %s", active_skills) - if logger is not None: - logger.log_retriever_plan( + queries_per_skill_meta = {name: len(qs) for name, qs in skill_queries.items()} + if on_activity is not None: + await on_activity( + { + "kind": "retrieval_plan_ready", + "phase": "retrieval", + "meta": { + "skills": active_skills, + "queries_per_skill": queries_per_skill_meta, + }, + } + ) + + if trace_logger is not None: + trace_logger.log_retriever_plan( system_prompt=system_prompt, user_prompt=user_prompt, raw_response=raw, @@ -217,6 +238,17 @@ async def _run_skill(skill_name: str, queries: list[str]) -> None: if skill is None: logger.warning(" skill '%s' not in retrieval registry — skipping", skill_name) return + if on_activity is not None: + await on_activity( + { + "kind": "retrieval_skill_started", + "phase": "retrieval", + "meta": { + "skill": skill_name, + "query_count": len(queries), + }, + } + ) node = PlanNode( node_id=f"retrieval_{skill_name}", description=query, @@ -245,8 +277,20 @@ async def _run_skill(skill_name: str, queries: list[str]) -> None: " [%s] %d sources → %d chunks%s", skill_name, summary["sources_found"], summary["chunks_stored"], gate_str, ) - if logger is not None: - logger.log_skill_result( + if on_activity is not None: + await on_activity( + { + "kind": "retrieval_skill_finished", + "phase": "retrieval", + "meta": { + "skill": skill_name, + "sources_found": summary.get("sources_found", 0), + "chunks_stored": summary.get("chunks_stored", 0), + }, + } + ) + if trace_logger is not None: + trace_logger.log_skill_result( skill_name=skill_name, queries=queries, sources_found=summary.get("sources_found", 0), diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..e4e8e12 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,110 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = db/migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 tzdata package (or backports.zoneinfo for python<3.9) +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during the 'revision' command, +# regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to db/migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:db/migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses +# os.pathsep. Note that the one used by legacy projects is : +# version_path_separator = os +# version_path_separator = ; +# version_path_separator = : +version_path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# New in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# sqlalchemy.url is intentionally left empty here. +# It is set programmatically in db/migrations/env.py from the DATABASE_URL environment variable. +sqlalchemy.url = + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, with a positional argument +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/auth/__init__.py b/api/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/auth/router.py b/api/auth/router.py new file mode 100644 index 0000000..8a2a4b5 --- /dev/null +++ b/api/auth/router.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, status +from sqlalchemy.ext.asyncio import AsyncSession + +from api.auth.schemas import GoogleAuthRequest, RefreshRequest, TokenPair +from api.auth.service import ( + create_sse_token, + create_token_pair, + revoke_refresh_token, + upsert_user, + verify_google_id_token, + verify_refresh_token, +) +from api.deps import get_current_user, get_db +from db.models import User + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/google", response_model=TokenPair, status_code=status.HTTP_201_CREATED) +async def google_auth( + body: GoogleAuthRequest, + db: AsyncSession = Depends(get_db), +) -> TokenPair: + """ + Exchange a Google ID token for a Singularity JWT pair. + + The Google token is verified server-side, the user row is upserted, + and a fresh access + refresh token pair is returned. + """ + google_payload = verify_google_id_token(body.id_token) + user = await upsert_user(db, google_payload) + return await create_token_pair(db, user.id) + + +@router.post("/refresh", response_model=TokenPair, status_code=status.HTTP_200_OK) +async def refresh_tokens( + body: RefreshRequest, + db: AsyncSession = Depends(get_db), +) -> TokenPair: + """ + Rotate a refresh token. + + The supplied token is revoked and a new pair is issued. If the token has + already been used (replay attack), the entire rotation family is revoked. + """ + _user, token_pair = await verify_refresh_token(db, body.refresh_token) + return token_pair + + +@router.post("/logout", status_code=status.HTTP_200_OK) +async def logout( + body: RefreshRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> dict: + """Revoke the supplied refresh token (effectively logging out the session).""" + await revoke_refresh_token(db, body.refresh_token) + return {"status": "logged_out"} + + +@router.get("/sse-token", status_code=status.HTTP_200_OK) +async def get_sse_token( + current_user: User = Depends(get_current_user), +) -> dict: + """ + Issue a short-lived (30 s) token for SSE endpoint authentication. + + Clients that cannot attach an Authorization header (e.g. EventSource) + should call this first and pass the returned token as a query parameter. + """ + token = create_sse_token(current_user.id) + return {"token": token, "expires_in": 30} diff --git a/api/auth/schemas.py b/api/auth/schemas.py new file mode 100644 index 0000000..d3bcded --- /dev/null +++ b/api/auth/schemas.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class GoogleAuthRequest(BaseModel): + id_token: str + + +class TokenPair(BaseModel): + access_token: str + refresh_token: str + token_type: str = "bearer" + expires_in: int # seconds until access token expiry + + +class RefreshRequest(BaseModel): + refresh_token: str diff --git a/api/auth/service.py b/api/auth/service.py new file mode 100644 index 0000000..f321a5c --- /dev/null +++ b/api/auth/service.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +import hashlib +import secrets +import uuid +from datetime import datetime, timedelta, timezone + +from fastapi import HTTPException, status +from google.auth.exceptions import GoogleAuthError +from google.auth.transport import requests as google_requests +from google.oauth2 import id_token as google_id_token +from jose import jwt +from sqlalchemy import select, update +from sqlalchemy.dialects.postgresql import insert as pg_insert +from sqlalchemy.ext.asyncio import AsyncSession + +from api.auth.schemas import TokenPair +from api.config import settings +from db.models import RefreshToken, User + + +def _hash_token(raw: str) -> str: + return hashlib.sha256(raw.encode()).hexdigest() + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +def _as_utc_aware(dt: datetime) -> datetime: + """ + Normalize ORM-loaded datetimes for comparison. + + Some drivers return naive timestamps even for TIMESTAMPTZ columns; _now() is + always UTC-aware, so comparisons must use the same awareness. + """ + if dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc) + + +# --------------------------------------------------------------------------- +# Google ID token verification +# --------------------------------------------------------------------------- + + +def verify_google_id_token(id_token: str) -> dict: + """ + Verify a Google ID token and return the decoded payload. + + Returns a dict with at minimum: sub, email, name, picture. + Raises HTTP 401 if verification fails. + """ + try: + request = google_requests.Request() + payload = google_id_token.verify_oauth2_token( + id_token, + request, + settings.google_client_id, + ) + except (GoogleAuthError, ValueError) as exc: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=f"Invalid Google ID token: {exc}", + ) from exc + + return payload + + +# --------------------------------------------------------------------------- +# User upsert +# --------------------------------------------------------------------------- + + +async def upsert_user(db: AsyncSession, google_payload: dict) -> User: + """ + Insert or update the user record based on the Google sub. + + On conflict (google_sub already exists) we update last_login_at, name, + and avatar_url to keep profile info fresh. + """ + sub: str = google_payload["sub"] + email: str = google_payload["email"] + name: str | None = google_payload.get("name") + avatar_url: str | None = google_payload.get("picture") + now = _now() + + stmt = ( + pg_insert(User) + .values( + id=uuid.uuid4(), + google_sub=sub, + email=email, + name=name, + avatar_url=avatar_url, + created_at=now, + last_login_at=now, + daily_token_budget=settings.default_daily_token_budget, + is_active=True, + ) + .on_conflict_do_update( + index_elements=["google_sub"], + set_={ + "last_login_at": now, + "name": name, + "avatar_url": avatar_url, + }, + ) + .returning(User) + ) + + result = await db.execute(stmt) + await db.commit() + user = result.scalar_one() + return user + + +# --------------------------------------------------------------------------- +# Token issuance +# --------------------------------------------------------------------------- + + +def _build_access_token(user_id: str) -> str: + expire = _now() + timedelta(minutes=settings.access_token_expire_minutes) + payload = { + "sub": user_id, + "exp": expire, + "iat": _now(), + "type": "access", + } + return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) + + +async def create_token_pair(db: AsyncSession, user_id: uuid.UUID) -> TokenPair: + """ + Issue a new JWT access token and an opaque refresh token. + + The refresh token is stored as a SHA-256 hash in refresh_tokens, + assigned to a new rotation family. + """ + access_token = _build_access_token(str(user_id)) + raw_refresh = secrets.token_urlsafe(48) + family_id = uuid.uuid4() + now = _now() + expires_at = now + timedelta(days=settings.refresh_token_expire_days) + + refresh_row = RefreshToken( + user_id=user_id, + token_hash=_hash_token(raw_refresh), + family_id=family_id, + created_at=now, + expires_at=expires_at, + ) + db.add(refresh_row) + await db.commit() + + return TokenPair( + access_token=access_token, + refresh_token=raw_refresh, + expires_in=settings.access_token_expire_minutes * 60, + ) + + +# --------------------------------------------------------------------------- +# Refresh token rotation +# --------------------------------------------------------------------------- + + +async def verify_refresh_token(db: AsyncSession, raw_token: str) -> tuple[User, TokenPair]: + """ + Validate the raw refresh token, rotate it (issue new pair + revoke old), + and return the authenticated user and new token pair. + + If the token has already been used (reuse detection), revoke the entire + family to force re-authentication. + """ + token_hash = _hash_token(raw_token) + now = _now() + + result = await db.execute( + select(RefreshToken).where(RefreshToken.token_hash == token_hash) + ) + rt: RefreshToken | None = result.scalar_one_or_none() + + if rt is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Refresh token not found", + ) + + if rt.revoked_at is not None: + # Token reuse detected — revoke entire family + await db.execute( + update(RefreshToken) + .where( + RefreshToken.family_id == rt.family_id, + RefreshToken.revoked_at.is_(None), + ) + .values(revoked_at=now) + ) + await db.commit() + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Refresh token already used — please log in again", + ) + + expires_at = _as_utc_aware(rt.expires_at) + if expires_at < now: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Refresh token expired", + ) + + # Revoke the consumed token + rt.revoked_at = now + await db.flush() + + # Load user + user_result = await db.execute(select(User).where(User.id == rt.user_id)) + user: User | None = user_result.scalar_one_or_none() + if user is None or not user.is_active: + await db.rollback() + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found or inactive", + ) + + # Issue new pair in the same family + access_token = _build_access_token(str(user.id)) + raw_refresh_new = secrets.token_urlsafe(48) + new_expires_at = now + timedelta(days=settings.refresh_token_expire_days) + + new_rt = RefreshToken( + user_id=user.id, + token_hash=_hash_token(raw_refresh_new), + family_id=rt.family_id, + created_at=now, + expires_at=new_expires_at, + ) + db.add(new_rt) + await db.commit() + + return user, TokenPair( + access_token=access_token, + refresh_token=raw_refresh_new, + expires_in=settings.access_token_expire_minutes * 60, + ) + + +# --------------------------------------------------------------------------- +# Logout +# --------------------------------------------------------------------------- + + +async def revoke_refresh_token(db: AsyncSession, raw_token: str) -> None: + """Mark a refresh token as revoked.""" + token_hash = _hash_token(raw_token) + result = await db.execute( + select(RefreshToken).where( + RefreshToken.token_hash == token_hash, + RefreshToken.revoked_at.is_(None), + ) + ) + rt: RefreshToken | None = result.scalar_one_or_none() + if rt is not None: + rt.revoked_at = _now() + await db.commit() + + +# --------------------------------------------------------------------------- +# Short-lived SSE token +# --------------------------------------------------------------------------- + + +def create_sse_token(user_id: uuid.UUID) -> str: + """ + Issue a 30-second single-use token for SSE endpoint authentication. + + The token is a compact JWT with type="sse" and a very short expiry. + """ + expire = _now() + timedelta(seconds=30) + payload = { + "sub": str(user_id), + "exp": expire, + "iat": _now(), + "type": "sse", + } + return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) diff --git a/api/config.py b/api/config.py new file mode 100644 index 0000000..4b68d55 --- /dev/null +++ b/api/config.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") + + # Database + database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/singularity" + + # Redis + redis_url: str = "redis://localhost:6379/0" + + # JWT + jwt_secret: str = "change-me-in-production" + jwt_algorithm: str = "HS256" + access_token_expire_minutes: int = 15 + refresh_token_expire_days: int = 30 + + # Google OAuth + google_client_id: str = "" + + # Blob storage + blob_store: str = "local" # "local" | "s3" + local_blob_dir: str = "./blob_storage" + s3_bucket: str = "" + s3_endpoint_url: str = "" + aws_access_key_id: str = "" + aws_secret_access_key: str = "" + + # Observability + sentry_dsn: str = "" + environment: str = "development" + + # CORS / Frontend (browser PUT/DELETE preflight must match the page origin) + frontend_url: str = "http://localhost:3000" + cors_origins: list[str] = [ + "http://localhost:3000", + "http://127.0.0.1:3000", + ] + + # BYOK: Fernet key (url-safe base64, 32 bytes); empty in dev falls back to JWT-derived key + llm_credentials_encryption_key: str = "" + + # Quotas + default_daily_token_budget: int = 1_000_000 + max_concurrent_jobs_per_user: int = 2 + + # Integration testing: mock research (no LLM). Requires allowlisted email + client flag. + debug_mock_research: bool = False + debug_mock_research_allow_email: str = "nish2002.sharma@gmail.com" + + +settings = Settings() diff --git a/api/debug_research_mock_policy.py b/api/debug_research_mock_policy.py new file mode 100644 index 0000000..b41c0f8 --- /dev/null +++ b/api/debug_research_mock_policy.py @@ -0,0 +1,75 @@ +""" +Gate for integration-test mock research (no LLM / pipeline). + +Requires a master switch (DEBUG_MOCK_RESEARCH=true, or ENVIRONMENT=development) +and an allowlisted account email (DEBUG_MOCK_RESEARCH_ALLOW_EMAIL). +""" +from __future__ import annotations + +from fastapi import HTTPException, status + +from api.config import settings +from db.models import User + +DEFAULT_ALLOW_EMAIL = "nish2002.sharma@gmail.com" + + +def _normalized_allow_email() -> str: + raw = (settings.debug_mock_research_allow_email or DEFAULT_ALLOW_EMAIL).strip() + return raw.lower() + + +def _debug_mock_master_enabled() -> bool: + """ + True when the API may honor client debug_mock flags (allowlisted user still required). + + In development, mock is allowed without DEBUG_MOCK_RESEARCH in .env so local dashboards + match the debug UI checkbox. Production should set ENVIRONMENT=production. + """ + if settings.debug_mock_research: + return True + return (settings.environment or "").strip().lower() == "development" + + +def debug_mock_eligible_user(user: User) -> bool: + """True if this user's email may use debug mock when the master switch is on.""" + if not _debug_mock_master_enabled(): + return False + email = (user.email or "").strip().lower() + return bool(email) and email == _normalized_allow_email() + + +def assert_debug_mock_request_allowed(user: User, debug_mock: bool) -> None: + """ + If the client asks for debug_mock, require master switch + allowlisted email. + Raises 403 when debug_mock is true but not permitted. + """ + if not debug_mock: + return + if not _debug_mock_master_enabled(): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=( + "Debug mock research is disabled on this server. " + "Set DEBUG_MOCK_RESEARCH=true in the API .env, or set ENVIRONMENT=development." + ), + ) + email = (user.email or "").strip().lower() + allow = _normalized_allow_email() + if not email or email != allow: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=( + "Debug mock is not allowed for this account. " + "Set DEBUG_MOCK_RESEARCH_ALLOW_EMAIL in the API .env to your Google login email " + f"(currently allowlisted: {allow})." + ), + ) + + +def use_debug_mock_research_job(user: User, debug_mock: bool) -> bool: + """True when the dashboard job should run the mock worker instead of the real pipeline.""" + if not debug_mock: + return False + assert_debug_mock_request_allowed(user, debug_mock) + return True diff --git a/api/deps.py b/api/deps.py new file mode 100644 index 0000000..7234aa9 --- /dev/null +++ b/api/deps.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator +from typing import Optional + +from arq import ArqRedis +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from api.config import settings +from db.models import User +from db.session import get_db + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/google", auto_error=False) + +# Module-level ARQ Redis pool — initialised during app lifespan. +# ArqRedis extends redis.asyncio.Redis so pub/sub and all standard +# Redis commands are available alongside .enqueue_job(). +_redis_pool: Optional[ArqRedis] = None + + +def set_redis_pool(pool: ArqRedis) -> None: + global _redis_pool + _redis_pool = pool + + +async def get_redis() -> AsyncGenerator[ArqRedis, None]: + if _redis_pool is None: + raise RuntimeError("Redis pool has not been initialised") + yield _redis_pool + + +async def _decode_user_from_token(token: str, db: AsyncSession) -> User: + credentials_exc = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) + user_id: str | None = payload.get("sub") + if user_id is None: + raise credentials_exc + except JWTError: + raise credentials_exc + + result = await db.execute(select(User).where(User.id == user_id)) + user: User | None = result.scalar_one_or_none() + if user is None: + raise credentials_exc + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="User account is inactive", + ) + return user + + +async def get_current_user( + token: str = Depends(oauth2_scheme), + db: AsyncSession = Depends(get_db), +) -> User: + if not token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + return await _decode_user_from_token(token, db) + + +async def get_current_user_optional( + token: str = Depends(oauth2_scheme), + db: AsyncSession = Depends(get_db), +) -> Optional[User]: + if not token: + return None + try: + return await _decode_user_from_token(token, db) + except HTTPException: + return None diff --git a/api/llm/router.py b/api/llm/router.py new file mode 100644 index 0000000..97bada1 --- /dev/null +++ b/api/llm/router.py @@ -0,0 +1,50 @@ +"""Public LLM model catalog for chat (BYOK).""" +from __future__ import annotations + +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession + +from api.deps import get_current_user, get_db +from api.llm_credentials_service import chat_models_for_providers, providers_with_valid_keys +from db.models import User + +router = APIRouter(prefix="/llm", tags=["llm"]) + + +class LlmModelOut(BaseModel): + model_id: str + display_name: str + provider: str + tags: list[str] + description: str + + +class LlmModelsResponse(BaseModel): + models: list[LlmModelOut] + + +@router.get("/models", response_model=LlmModelsResponse) +async def list_llm_models( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> LlmModelsResponse: + """ + Chat models the current user can run with their stored keys only. + + Excludes models whose provider (or Grok, used by the thinker) has no key. + """ + prov = await providers_with_valid_keys(db, current_user.id) + eligible = chat_models_for_providers(prov) + return LlmModelsResponse( + models=[ + LlmModelOut( + model_id=m.model_id, + display_name=m.display_name, + provider=m.provider, + tags=list(m.tags), + description=m.description, + ) + for m in eligible + ] + ) diff --git a/api/llm_credentials_service.py b/api/llm_credentials_service.py new file mode 100644 index 0000000..88e213e --- /dev/null +++ b/api/llm_credentials_service.py @@ -0,0 +1,205 @@ +""" +Load and validate user LLM API keys (BYOK) for chat and research. +""" +from __future__ import annotations + +import uuid +from datetime import datetime, timezone + +from fastapi import HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from agents.chat.models import ( + AVAILABLE_MODELS, + DEFAULT_MODEL_ID, + MODEL_MAP, + ModelInfo, +) +from api.llm_secret_crypto import decrypt_llm_secret, encrypt_llm_secret, is_decrypt_error +from db.models import UserLlmCredential + +ALLOWED_PROVIDERS: frozenset[str] = frozenset({"grok", "gemini", "deepseek"}) + +def normalize_provider(raw: str) -> str: + p = raw.strip().lower() + if p not in ALLOWED_PROVIDERS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid provider. Allowed: {', '.join(sorted(ALLOWED_PROVIDERS))}", + ) + return p + + +def model_provider(model_id: str) -> str: + info: ModelInfo | None = MODEL_MAP.get(model_id) + if info is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Unknown model_id: {model_id}", + ) + return info.provider + + +def validate_model_id(model_id: str) -> str: + mid = (model_id or "").strip() or DEFAULT_MODEL_ID + if mid not in MODEL_MAP: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Unknown model_id: {mid}", + ) + return mid + + +def last_four(secret: str) -> str: + s = secret.strip() + if len(s) <= 4: + return "****" + return s[-4:] + + +async def get_credential_row( + db: AsyncSession, + user_id: uuid.UUID, + provider: str, +) -> UserLlmCredential | None: + result = await db.execute( + select(UserLlmCredential).where( + UserLlmCredential.user_id == user_id, + UserLlmCredential.provider == provider, + ) + ) + return result.scalar_one_or_none() + + +async def get_decrypted_provider_key( + db: AsyncSession, + user_id: uuid.UUID, + provider: str, +) -> str | None: + row = await get_credential_row(db, user_id, provider) + if row is None: + return None + try: + return decrypt_llm_secret(row.encrypted_secret) + except Exception as e: + if is_decrypt_error(e): + return None + raise + + +def credential_row_to_public(row: UserLlmCredential) -> dict: + try: + plain = decrypt_llm_secret(row.encrypted_secret) + lf = last_four(plain) + except Exception: + lf = "????" + return { + "id": row.id, + "provider": row.provider, + "label": row.label, + "last_four": lf, + "created_at": row.created_at, + "updated_at": row.updated_at, + } + + +async def list_credentials_public(db: AsyncSession, user_id: uuid.UUID) -> list[dict]: + result = await db.execute( + select(UserLlmCredential) + .where(UserLlmCredential.user_id == user_id) + .order_by(UserLlmCredential.provider) + ) + rows = list(result.scalars().all()) + return [credential_row_to_public(r) for r in rows] + + +async def upsert_credential( + db: AsyncSession, + user_id: uuid.UUID, + provider: str, + secret: str, + label: str | None, +) -> UserLlmCredential: + secret = secret.strip() + if len(secret) < 8: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="API key is too short", + ) + enc = encrypt_llm_secret(secret) + existing = await get_credential_row(db, user_id, provider) + if existing: + existing.encrypted_secret = enc + existing.label = label + existing.updated_at = datetime.now(timezone.utc) + await db.commit() + await db.refresh(existing) + return existing + row = UserLlmCredential( + user_id=user_id, + provider=provider, + encrypted_secret=enc, + label=label, + ) + db.add(row) + await db.commit() + await db.refresh(row) + return row + + +async def delete_credential( + db: AsyncSession, + user_id: uuid.UUID, + provider: str, +) -> bool: + row = await get_credential_row(db, user_id, provider) + if row is None: + return False + await db.delete(row) + await db.commit() + return True + + +async def providers_with_valid_keys( + db: AsyncSession, + user_id: uuid.UUID, +) -> frozenset[str]: + """ + Return each allowed provider for which the user has a stored, decryptable secret. + + Used to filter chat model options: only providers with a saved key are listed. + """ + out: set[str] = set() + for prov in sorted(ALLOWED_PROVIDERS): + k = await get_decrypted_provider_key(db, user_id, prov) + if k: + out.add(prov) + return frozenset(out) + + +def chat_models_for_providers(providers_with_keys: frozenset[str]) -> list[ModelInfo]: + """Registered chat models for providers the user has configured (BYOK).""" + return [m for m in AVAILABLE_MODELS if m.provider in providers_with_keys] + + +async def require_provider_key_for_model( + db: AsyncSession, + user_id: uuid.UUID, + model_id: str, +) -> str: + """ + Return the decrypted API key for the chat model's provider. + + Raises: + HTTPException: 400 if the user has no key for that provider. + """ + mid = validate_model_id(model_id) + prov = model_provider(mid) + k = await get_decrypted_provider_key(db, user_id, prov) + if not k: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Add your {prov} API key in Profile → LLM keys to use this model.", + ) + return k diff --git a/api/llm_secret_crypto.py b/api/llm_secret_crypto.py new file mode 100644 index 0000000..81a58fe --- /dev/null +++ b/api/llm_secret_crypto.py @@ -0,0 +1,45 @@ +""" +Encrypt/decrypt user LLM API keys at rest (Fernet symmetric). + +Uses ``Settings.llm_credentials_encryption_key`` when set; in ``development`` +with an empty key, derives a deterministic key from ``jwt_secret`` so local +dev works without a separate env var (not for production). +""" +from __future__ import annotations + +import base64 +import hashlib + +from cryptography.fernet import Fernet, InvalidToken + +from api.config import settings + + +def _fernet_key_material() -> bytes: + raw = (settings.llm_credentials_encryption_key or "").strip() + if raw: + return raw.encode("utf-8") + if settings.environment == "development": + digest = hashlib.sha256(settings.jwt_secret.encode("utf-8")).digest() + return base64.urlsafe_b64encode(digest) + raise RuntimeError( + "LLM_CREDENTIALS_ENCRYPTION_KEY is required when ENVIRONMENT is not development" + ) + + +def _fernet() -> Fernet: + return Fernet(_fernet_key_material()) + + +def encrypt_llm_secret(plaintext: str) -> str: + """Return url-safe token string to store in the database.""" + return _fernet().encrypt(plaintext.encode("utf-8")).decode("ascii") + + +def decrypt_llm_secret(token: str) -> str: + """Decrypt DB token; raises InvalidToken on tampering or wrong key.""" + return _fernet().decrypt(token.encode("ascii")).decode("utf-8") + + +def is_decrypt_error(exc: BaseException) -> bool: + return isinstance(exc, InvalidToken) diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000..51f652b --- /dev/null +++ b/api/main.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +import logging +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +from arq import ArqRedis, create_pool +from arq.connections import RedisSettings +from fastapi import Depends, FastAPI +from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession + +from api.auth.router import router as auth_router +from api.llm.router import router as llm_router +from api.config import settings +from api.deps import get_db, get_redis, set_redis_pool +from api.middleware.auth import AuthMiddleware +from api.middleware.rate_limit import RateLimitMiddleware +from api.middleware.request_id import RequestIDMiddleware +from api.middleware.usage_emitter import UsageEmitterMiddleware +from api.reports.router import router as reports_router +from api.research.router import router as research_router +from api.threads.router import router as threads_router +from api.users.router import router as users_router +from db.models import Base +from db.session import engine + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + # ------------------------------------------------------------------ startup + logger.info("Starting Singularity API — environment: %s", settings.environment) + + # In development: auto-create all tables. + # In production Alembic migrations are the sole schema authority. + if settings.environment == "development": + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + logger.info("Database tables ensured (dev mode)") + + # Initialise ARQ Redis pool. + # ArqRedis extends redis.asyncio.Redis, so all standard commands + # (ping, publish, subscribe) plus .enqueue_job() are available. + redis_pool: ArqRedis = await create_pool(RedisSettings.from_dsn(settings.redis_url)) + set_redis_pool(redis_pool) + logger.info("ARQ Redis pool initialised") + + # Initialise Sentry if configured + if settings.sentry_dsn: + import sentry_sdk + from sentry_sdk.integrations.fastapi import FastApiIntegration + from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration + + sentry_sdk.init( + dsn=settings.sentry_dsn, + environment=settings.environment, + integrations=[FastApiIntegration(), SqlalchemyIntegration()], + traces_sample_rate=0.2, + ) + logger.info("Sentry initialised") + + yield + + # ----------------------------------------------------------------- shutdown + logger.info("Shutting down Singularity API") + await redis_pool.aclose() + await engine.dispose() + logger.info("Connections closed") + + +# --------------------------------------------------------------------------- +# Application factory +# --------------------------------------------------------------------------- + +app = FastAPI( + title="Singularity API", + version="1.0.0", + description="AI-powered research platform — backend API", + lifespan=lifespan, + docs_url="/docs" if settings.environment == "development" else None, + redoc_url="/redoc" if settings.environment == "development" else None, +) + +# --------------------------------------------------------------------------- +# Middleware stack (add_middleware applies in reverse — last-added runs first) +# --------------------------------------------------------------------------- + +# Innermost (runs closest to route handlers) +app.add_middleware(UsageEmitterMiddleware) +app.add_middleware(AuthMiddleware) +app.add_middleware(RateLimitMiddleware) +app.add_middleware(RequestIDMiddleware) + +# CORS — must wrap everything. +# In development, allow any localhost / 127.0.0.1 port so PUT (e.g. BYOK keys) works when the +# Next app runs on a non-3000 port or the user opens 127.0.0.1 vs localhost. +_cors_kw: dict = { + "allow_credentials": True, + "allow_methods": ["*"], + "allow_headers": ["*"], + "expose_headers": ["X-Request-ID"], +} +if settings.environment == "development": + _cors_kw["allow_origin_regex"] = r"^http://(localhost|127\.0\.0\.1)(:\d+)?$" +else: + _cors_kw["allow_origins"] = settings.cors_origins +app.add_middleware(CORSMiddleware, **_cors_kw) + +# Sentry request monitoring is registered via sentry_sdk.init() during lifespan +# and attaches automatically as ASGI middleware. + +# --------------------------------------------------------------------------- +# Routers +# --------------------------------------------------------------------------- + +app.include_router(auth_router, prefix="/api/v1") +app.include_router(research_router, prefix="/api/v1") +app.include_router(reports_router, prefix="/api/v1") +app.include_router(threads_router, prefix="/api/v1") +app.include_router(users_router, prefix="/api/v1") +app.include_router(llm_router, prefix="/api/v1") + + +# --------------------------------------------------------------------------- +# Health / readiness endpoints +# --------------------------------------------------------------------------- + + +@app.get("/api/health", tags=["ops"]) +async def health() -> dict: + """Lightweight liveness probe — always 200 when the process is up.""" + return {"status": "ok"} + + +@app.get("/api/ready", tags=["ops"]) +async def ready( + db: AsyncSession = Depends(get_db), + redis: ArqRedis = Depends(get_redis), +) -> dict: + """ + Readiness probe — verifies connectivity to PostgreSQL and Redis. + + Returns 200 only when both data stores respond successfully. + """ + await db.execute(text("SELECT 1")) + await redis.ping() + return {"status": "ready"} diff --git a/api/middleware/__init__.py b/api/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/middleware/auth.py b/api/middleware/auth.py new file mode 100644 index 0000000..13f0f33 --- /dev/null +++ b/api/middleware/auth.py @@ -0,0 +1,90 @@ +""" +JWT authentication middleware. + +Validates Bearer tokens on every request to /api/v1/* (except +explicitly whitelisted paths like /auth/google). Attaches the +decoded user_id to ``request.state`` so downstream dependencies +and the usage emitter can read it without re-decoding. +""" +from __future__ import annotations + +import logging +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.requests import Request +from starlette.responses import JSONResponse, Response + +from jose import JWTError, jwt + +from api.config import settings + +logger = logging.getLogger(__name__) + +# Paths that do NOT require authentication. +_PUBLIC_PREFIXES = ( + "/api/v1/auth/google", + "/api/v1/auth/refresh", + "/docs", + "/redoc", + "/openapi.json", + "/api/health", + "/api/ready", +) + + +class AuthMiddleware(BaseHTTPMiddleware): + """ + Lightweight JWT guard that runs before route matching. + + For requests that carry a valid ``Authorization: Bearer `` + header the ``sub`` claim is stored in ``request.state.user_id``. + + Requests to public endpoints bypass validation entirely. + """ + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + path = request.url.path + + # Skip auth for public endpoints and non-API routes + if not path.startswith("/api/v1/") or any(path.startswith(p) for p in _PUBLIC_PREFIXES): + return await call_next(request) + + # SSE endpoints handle their own auth via query-param token + if path.endswith("/events") or path.endswith("/messages"): + return await call_next(request) + + auth_header = request.headers.get("Authorization") + if not auth_header or not auth_header.startswith("Bearer "): + logger.debug("Auth rejected for %s — missing/invalid Authorization header", path) + return JSONResponse( + status_code=401, + content={"detail": "Missing or invalid Authorization header"}, + ) + + token = auth_header.split(" ", 1)[1] + try: + payload = jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) + except JWTError: + return JSONResponse( + status_code=401, + content={"detail": "Invalid or expired token"}, + ) + + token_type = payload.get("type", "access") + if token_type != "access": + return JSONResponse( + status_code=401, + content={"detail": f"Expected access token, got {token_type}"}, + ) + + user_id = payload.get("sub") + if not user_id: + return JSONResponse( + status_code=401, + content={"detail": "Token missing subject claim"}, + ) + + # Attach for downstream use (deps.get_current_user still does full validation) + request.state.user_id = user_id + request.state.token_payload = payload + + return await call_next(request) diff --git a/api/middleware/rate_limit.py b/api/middleware/rate_limit.py new file mode 100644 index 0000000..2d61424 --- /dev/null +++ b/api/middleware/rate_limit.py @@ -0,0 +1,118 @@ +""" +Redis-backed sliding-window rate limiter middleware. + +Uses a sorted-set per client key to track request timestamps. +Configurable via settings (or env vars): + + RATE_LIMIT_RPM=60 # requests per minute + RATE_LIMIT_BURST=10 # burst allowance + +Pinned to per-user limits when ``request.state.user_id`` is available +(post-auth), otherwise falls back to client IP. +""" +from __future__ import annotations + +import logging +import time +from typing import Optional + +from arq import ArqRedis +from redis.exceptions import ConnectionError as RedisConnectionError +from redis.exceptions import TimeoutError as RedisTimeoutError +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.requests import Request +from starlette.responses import JSONResponse, Response + +from api.config import settings + +logger = logging.getLogger(__name__) + +# How many requests allowed per sliding window (60 s by default). +_RATE_LIMIT_RPM: int = getattr(settings, "rate_limit_rpm", 60) +_WINDOW_SECONDS: int = 60 + + +class RateLimitMiddleware(BaseHTTPMiddleware): + """ + Sliding-window rate limiter backed by Redis sorted sets. + + Each client is identified by user_id (when authenticated) or + source IP as a fallback. The middleware maintains a sorted set + of timestamps in Redis and trims entries older than the window + before checking the count against the limit. + + If Redis is unreachable (timeout/connection errors), requests pass through + without limiting so the API remains available; a warning is logged. + """ + + def __init__(self, app, redis_getter=None): + super().__init__(app) + # Allow injection of a redis getter for testing; production + # reads from the global pool set in api.deps. + self._get_redis = redis_getter + + async def _get_redis_pool(self) -> Optional[ArqRedis]: + if self._get_redis: + return await self._get_redis() + # Lazy import to avoid circular references at module level + from api.deps import _redis_pool + return _redis_pool + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + # Only rate-limit API routes + if not request.url.path.startswith("/api/v1/"): + return await call_next(request) + + redis: Optional[ArqRedis] = await self._get_redis_pool() + if redis is None: + # Redis not ready yet (e.g. during startup) — pass through + return await call_next(request) + + client_key = self._client_key(request) + now = time.time() + window_start = now - _WINDOW_SECONDS + key = f"ratelimit:{client_key}" + + pipe = redis.pipeline(transaction=True) + # Remove expired entries + pipe.zremrangebyscore(key, 0, window_start) + # Count current window entries + pipe.zcard(key) + # Add current request + pipe.zadd(key, {str(now): now}) + # Set TTL on the key to auto-clean + pipe.expire(key, _WINDOW_SECONDS + 1) + try: + results = await pipe.execute() + except (RedisTimeoutError, RedisConnectionError) as exc: + # Redis down/slow would otherwise 500 every /api/v1 request; fail-open until Redis is healthy. + logger.warning( + "Rate limit skipped (Redis unreachable): %s client_key=%s", + exc, + client_key, + ) + return await call_next(request) + + current_count: int = results[1] + + if current_count >= _RATE_LIMIT_RPM: + logger.warning("Rate limit exceeded for %s (%d requests in window)", client_key, current_count) + return JSONResponse( + status_code=429, + content={ + "detail": "Rate limit exceeded. Please slow down.", + "retry_after_seconds": _WINDOW_SECONDS, + }, + ) + + return await call_next(request) + + @staticmethod + def _client_key(request: Request) -> str: + """Use authenticated user_id when available, else IP.""" + user_id: str | None = getattr(request.state, "user_id", None) + if user_id: + return f"user:{user_id}" + forwarded = request.headers.get("X-Forwarded-For", "") + ip = forwarded.split(",")[0].strip() if forwarded else (request.client.host if request.client else "unknown") + return f"ip:{ip}" diff --git a/api/middleware/request_id.py b/api/middleware/request_id.py new file mode 100644 index 0000000..e69cecd --- /dev/null +++ b/api/middleware/request_id.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import uuid + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response +from starlette.types import ASGIApp + + +class RequestIDMiddleware(BaseHTTPMiddleware): + """ + Propagate or generate a unique request ID for every request. + + - Reads `X-Request-ID` from the incoming request headers if present. + - Falls back to a freshly generated UUID4. + - Attaches the ID to `request.state.request_id`. + - Adds `X-Request-ID` to the outgoing response headers. + """ + + def __init__(self, app: ASGIApp) -> None: + super().__init__(app) + + async def dispatch(self, request: Request, call_next) -> Response: + request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4()) + request.state.request_id = request_id + + response = await call_next(request) + response.headers["X-Request-ID"] = request_id + return response diff --git a/api/middleware/usage_emitter.py b/api/middleware/usage_emitter.py new file mode 100644 index 0000000..2dfa49a --- /dev/null +++ b/api/middleware/usage_emitter.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import asyncio +import logging +import time +import uuid +from typing import Optional + +import user_agents +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response +from starlette.types import ASGIApp + +from db.models import UsageEvent +from db.session import AsyncSessionLocal + +logger = logging.getLogger(__name__) + + +def _parse_user_agent(ua_string: str) -> tuple[str, str, str]: + """ + Parse a User-Agent string using the user_agents library. + + Returns (device_type, os, browser). + """ + if not ua_string: + return "unknown", "unknown", "unknown" + + ua = user_agents.parse(ua_string) + + if ua.is_mobile: + device_type = "mobile" + elif ua.is_tablet: + device_type = "tablet" + else: + device_type = "desktop" + + os_name = ua.os.family or "unknown" + browser_name = ua.browser.family or "unknown" + + return device_type, os_name, browser_name + + +def _get_client_ip(request: Request) -> str: + """Extract the real client IP, respecting X-Forwarded-For.""" + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + return forwarded_for.split(",")[0].strip() + if request.client: + return request.client.host + return "" + + +def _extract_user_id(request: Request) -> Optional[uuid.UUID]: + """Attempt to read the authenticated user ID from request state.""" + user = getattr(request.state, "user", None) + if user is not None: + return getattr(user, "id", None) + return None + + +async def _emit_usage_event( + user_id: Optional[uuid.UUID], + event_type: str, + route: Optional[str], + duration_ms: int, + success: bool, + status_code: int, + ua_string: str, + ip_address: str, + request_id: Optional[str], +) -> None: + """ + Persist a UsageEvent row in its own short-lived session. + + This runs as a fire-and-forget task — failures are logged but never + propagate to the request/response cycle. + """ + if user_id is None: + return + + device_type, os_name, browser = _parse_user_agent(ua_string) + error_code = str(status_code) if not success else None + + try: + async with AsyncSessionLocal() as session: + event = UsageEvent( + user_id=user_id, + session_id=request_id, + event_type=event_type, + route=route, + duration_ms=duration_ms, + success=success, + error_code=error_code, + user_agent=ua_string or None, + ip_address=ip_address or None, + device_type=device_type, + os=os_name, + browser=browser, + ) + session.add(event) + await session.commit() + except Exception: + logger.exception("Failed to emit usage event for user %s", user_id) + + +class UsageEmitterMiddleware(BaseHTTPMiddleware): + """ + After every response, fire-and-forget a UsageEvent record. + + The task is scheduled with asyncio.create_task so it never delays + the response reaching the client. + """ + + # Routes to skip tracking (e.g. health / readiness probes) + _SKIP_PATHS = {"/api/health", "/api/ready", "/docs", "/openapi.json", "/redoc"} + + def __init__(self, app: ASGIApp) -> None: + super().__init__(app) + + async def dispatch(self, request: Request, call_next) -> Response: + if request.url.path in self._SKIP_PATHS: + return await call_next(request) + + start_ms = time.monotonic() + response: Response = await call_next(request) + elapsed_ms = int((time.monotonic() - start_ms) * 1000) + + user_id = _extract_user_id(request) + ua_string = request.headers.get("User-Agent", "") + ip_address = _get_client_ip(request) + request_id: Optional[str] = getattr(request.state, "request_id", None) + route = f"{request.method} {request.url.path}" + success = response.status_code < 400 + + # Classify event_type from path + path = request.url.path + if "/research/jobs" in path: + event_type = "research_job" + elif "/threads" in path: + event_type = "chat_message" + elif "/reports" in path: + if "/patch" in path: + event_type = "patch" + else: + event_type = "report_view" + elif "/auth" in path: + event_type = "auth" + else: + event_type = "api_request" + + asyncio.create_task( + _emit_usage_event( + user_id=user_id, + event_type=event_type, + route=route, + duration_ms=elapsed_ms, + success=success, + status_code=response.status_code, + ua_string=ua_string, + ip_address=ip_address, + request_id=request_id, + ) + ) + + return response diff --git a/api/reports/router.py b/api/reports/router.py new file mode 100644 index 0000000..ea222af --- /dev/null +++ b/api/reports/router.py @@ -0,0 +1,233 @@ +"""Reports API router — CRUD, versions, export.""" +from __future__ import annotations + +import uuid +from typing import Optional + +from arq import ArqRedis +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import StreamingResponse +from sqlalchemy.ext.asyncio import AsyncSession + +from api.deps import get_current_user, get_db, get_redis +from api.reports.schemas import ( + PatchRequest, + PatchResponse, + ReportListResponse, + ReportMeta, + VersionContent, + VersionListResponse, + VersionMeta, +) +from api.reports.service import ( + create_version, + delete_report, + get_report, + get_version_content, + list_reports, + list_versions, + load_content, +) +from api.threads.schemas import ThreadResponse +from api.threads.service import get_or_create_default_report_thread +from db.models import User + +router = APIRouter(prefix="/reports", tags=["reports"]) + + +def _report_to_meta(report, latest_version=None) -> ReportMeta: + return ReportMeta( + id=report.id, + title=report.title, + query=report.query, + strength=report.strength, + created_at=report.created_at, + latest_version=latest_version.version_num if latest_version else None, + latest_char_count=latest_version.char_count if latest_version else None, + ) + + +@router.get("", response_model=ReportListResponse) +async def list_user_reports( + cursor: Optional[str] = Query(None), + limit: int = Query(20, ge=1, le=50), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> ReportListResponse: + """Cursor-paginated list of the user's reports, newest first.""" + reports, next_cursor = await list_reports(db, current_user.id, cursor, limit) + items = [] + for r in reports: + from api.reports.service import get_latest_version + latest = await get_latest_version(db, r.id) + items.append(_report_to_meta(r, latest)) + return ReportListResponse(items=items, next_cursor=next_cursor) + + +@router.get("/{report_id}/threads/default", response_model=ThreadResponse) +async def get_or_create_default_report_thread_route( + report_id: uuid.UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> ThreadResponse: + """Canonical Q&A thread for this report (creates on first use).""" + thread = await get_or_create_default_report_thread(db, current_user.id, report_id) + return ThreadResponse( + id=thread.id, + report_id=thread.report_id, + pinned_version_num=thread.pinned_version_num, + canonical_report_qa=bool(thread.canonical_report_qa), + created_at=thread.created_at, + ) + + +@router.get("/{report_id}", response_model=ReportMeta) +async def get_report_meta( + report_id: uuid.UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> ReportMeta: + """Retrieve metadata for a single report.""" + report = await get_report(db, report_id, current_user.id) + from api.reports.service import get_latest_version + latest = await get_latest_version(db, report_id) + return _report_to_meta(report, latest) + + +@router.delete("/{report_id}", status_code=status.HTTP_200_OK) +async def delete_user_report( + report_id: uuid.UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> dict: + """Delete a report and all its versions.""" + await delete_report(db, report_id, current_user.id) + return {"status": "deleted"} + + +@router.get("/{report_id}/versions", response_model=VersionListResponse) +async def list_report_versions( + report_id: uuid.UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> VersionListResponse: + """List all versions of a report.""" + versions = await list_versions(db, report_id, current_user.id) + return VersionListResponse( + report_id=report_id, + versions=[ + VersionMeta( + version_num=v.version_num, + char_count=v.char_count, + patch_instruction=v.patch_instruction, + created_at=v.created_at, + ) + for v in versions + ], + ) + + +@router.get("/{report_id}/versions/{version_num}", response_model=VersionContent) +async def get_version( + report_id: uuid.UUID, + version_num: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> VersionContent: + """Get the full content of a specific report version.""" + version = await get_version_content(db, report_id, version_num, current_user.id) + content = await load_content(version) + return VersionContent( + version_num=version.version_num, + content=content, + etag=version.content_hash, + char_count=version.char_count, + ) + + +@router.post("/{report_id}/versions/{version_num}/patch", response_model=PatchResponse, status_code=status.HTTP_201_CREATED) +async def patch_report_version( + report_id: uuid.UUID, + version_num: int, + body: PatchRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), + redis: ArqRedis = Depends(get_redis), +) -> PatchResponse: + """ + Submit an edit instruction for a section of the report. + + Uses optimistic locking via If-Match (ETag). Returns 409 on conflict. + The actual patch is processed asynchronously; the response returns + once the patch job is enqueued. + """ + version = await get_version_content(db, report_id, version_num, current_user.id) + + # Optimistic locking check + if version.content_hash != body.if_match: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Content has been modified since you last read it. Please refresh.", + ) + + # Verify selected text exists in content + content = await load_content(version) + if body.selected_text not in content: + import difflib + ratio = difflib.SequenceMatcher(None, body.selected_text, content).quick_ratio() + if ratio < 0.85: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Selected text not found in report content", + ) + + # Enqueue patch job + job = await redis.enqueue_job( + "run_patch_job", + str(report_id), + version_num, + str(current_user.id), + body.selected_text, + body.instruction, + body.if_match, + ) + + return PatchResponse( + new_version_num=version_num + 1, + etag="pending", + char_count=0, + ) + + +@router.get("/{report_id}/versions/{version_num}/export") +async def export_version( + report_id: uuid.UUID, + version_num: int, + format: str = Query("md", regex="^(md|html)$"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> StreamingResponse: + """Export a report version as markdown or HTML.""" + version = await get_version_content(db, report_id, version_num, current_user.id) + content = await load_content(version) + + if format == "html": + import markdown as md_lib + html_content = md_lib.markdown(content, extensions=["tables", "fenced_code", "toc"]) + export_content = f"Report{html_content}" + media_type = "text/html" + filename = f"report_v{version_num}.html" + else: + export_content = content + media_type = "text/markdown" + filename = f"report_v{version_num}.md" + + import io + buffer = io.BytesIO(export_content.encode("utf-8")) + + from fastapi.responses import StreamingResponse + return StreamingResponse( + buffer, + media_type=media_type, + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) diff --git a/api/reports/schemas.py b/api/reports/schemas.py new file mode 100644 index 0000000..16ab786 --- /dev/null +++ b/api/reports/schemas.py @@ -0,0 +1,70 @@ +"""Pydantic schemas for the Reports API.""" +from __future__ import annotations + +from datetime import datetime +from typing import Optional +from uuid import UUID + +from pydantic import BaseModel, Field + + +# --------------------------------------------------------------------------- +# Report list / meta +# --------------------------------------------------------------------------- + +class ReportMeta(BaseModel): + id: UUID + title: Optional[str] + query: str + strength: int + created_at: datetime + latest_version: Optional[int] = None + latest_char_count: Optional[int] = None + + model_config = {"from_attributes": True} + + +class ReportListResponse(BaseModel): + items: list[ReportMeta] + next_cursor: Optional[str] = None + + +# --------------------------------------------------------------------------- +# Report versions +# --------------------------------------------------------------------------- + +class VersionMeta(BaseModel): + version_num: int + char_count: int + patch_instruction: Optional[str] + created_at: datetime + + model_config = {"from_attributes": True} + + +class VersionListResponse(BaseModel): + report_id: UUID + versions: list[VersionMeta] + + +class VersionContent(BaseModel): + version_num: int + content: str + etag: str # SHA-256 content hash + char_count: int + + +# --------------------------------------------------------------------------- +# Patch request +# --------------------------------------------------------------------------- + +class PatchRequest(BaseModel): + selected_text: str = Field(..., min_length=1, max_length=50000) + instruction: str = Field(..., min_length=1, max_length=2000) + if_match: str # ETag for optimistic locking + + +class PatchResponse(BaseModel): + new_version_num: int + etag: str + char_count: int diff --git a/api/reports/service.py b/api/reports/service.py new file mode 100644 index 0000000..bc5bd27 --- /dev/null +++ b/api/reports/service.py @@ -0,0 +1,256 @@ +""" +Report service layer. + +Handles CRUD for reports and versions, blob storage integration, +and optimistic-locking patch coordination. +""" +from __future__ import annotations + +import hashlib +import uuid +from datetime import datetime, timezone +from typing import Optional + +from fastapi import HTTPException, status +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from db.models import Report, ReportVersion, User +from storage import get_blob_store + +# Content size threshold: inline if smaller, blob otherwise. +_INLINE_THRESHOLD = 500_000 # ~500 KB + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +def _content_hash(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + +# --------------------------------------------------------------------------- +# Report listing (cursor-paginated) +# --------------------------------------------------------------------------- + +async def list_reports( + db: AsyncSession, + user_id: uuid.UUID, + cursor: Optional[str] = None, + limit: int = 20, +) -> tuple[list[Report], Optional[str]]: + """ + Return a page of reports for the user ordered by created_at DESC. + + ``cursor`` is an ISO-format timestamp of the last item's created_at. + """ + query = ( + select(Report) + .where(Report.user_id == user_id) + .order_by(Report.created_at.desc()) + .limit(limit + 1) + ) + if cursor: + query = query.where(Report.created_at < datetime.fromisoformat(cursor)) + + result = await db.execute(query) + reports = list(result.scalars().all()) + + next_cursor = None + if len(reports) > limit: + reports = reports[:limit] + next_cursor = reports[-1].created_at.isoformat() + + return reports, next_cursor + + +async def get_report( + db: AsyncSession, + report_id: uuid.UUID, + user_id: uuid.UUID, +) -> Report: + """Fetch a single report with ownership check.""" + result = await db.execute( + select(Report).where(Report.id == report_id, Report.user_id == user_id) + ) + report: Report | None = result.scalar_one_or_none() + if report is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Report not found") + return report + + +# --------------------------------------------------------------------------- +# Version management +# --------------------------------------------------------------------------- + +async def get_latest_version( + db: AsyncSession, + report_id: uuid.UUID, +) -> Optional[ReportVersion]: + result = await db.execute( + select(ReportVersion) + .where(ReportVersion.report_id == report_id) + .order_by(ReportVersion.version_num.desc()) + .limit(1) + ) + return result.scalar_one_or_none() + + +async def list_versions( + db: AsyncSession, + report_id: uuid.UUID, + user_id: uuid.UUID, +) -> list[ReportVersion]: + """List all versions for a report (ownership checked).""" + await get_report(db, report_id, user_id) + result = await db.execute( + select(ReportVersion) + .where(ReportVersion.report_id == report_id) + .order_by(ReportVersion.version_num.asc()) + ) + return list(result.scalars().all()) + + +async def get_version_content( + db: AsyncSession, + report_id: uuid.UUID, + version_num: int, + user_id: uuid.UUID, +) -> ReportVersion: + """Get a specific version with content loaded from inline or blob storage.""" + await get_report(db, report_id, user_id) + + result = await db.execute( + select(ReportVersion).where( + ReportVersion.report_id == report_id, + ReportVersion.version_num == version_num, + ) + ) + version: ReportVersion | None = result.scalar_one_or_none() + if version is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Version not found") + return version + + +async def load_content(version: ReportVersion) -> str: + """ + Load the markdown content for a version from inline storage or blob. + """ + if version.content_inline is not None: + return version.content_inline + if version.content_uri is None: + return "" + store = get_blob_store() + key = version.content_uri.replace("local://", "").replace("s3://", "") + return await store.read(key) + + +async def create_version( + db: AsyncSession, + report_id: uuid.UUID, + markdown: str, + patch_instruction: Optional[str] = None, +) -> ReportVersion: + """ + Create a new immutable version for a report. + + Content is stored inline if < 500 KB, otherwise written to blob storage. + """ + content_hash = _content_hash(markdown) + char_count = len(markdown) + + # Determine next version number + latest = await get_latest_version(db, report_id) + version_num = (latest.version_num + 1) if latest else 1 + + content_inline: Optional[str] = None + content_uri: Optional[str] = None + + if char_count < _INLINE_THRESHOLD: + content_inline = markdown + else: + store = get_blob_store() + key = f"reports/{report_id}/v{version_num}.md" + uri = await store.write(key, markdown) + content_uri = uri + + version = ReportVersion( + report_id=report_id, + version_num=version_num, + content_inline=content_inline, + content_uri=content_uri, + content_hash=content_hash, + char_count=char_count, + patch_instruction=patch_instruction, + created_at=_now(), + ) + db.add(version) + + # Auto-set report title from first heading if not set + report = await get_report(db, report_id, (await db.execute( + select(Report).where(Report.id == report_id) + )).scalar_one().user_id) + if not report.title: + for line in markdown.splitlines(): + if line.startswith("# "): + report.title = line[2:].strip() + break + + await db.commit() + await db.refresh(version) + return version + + +# --------------------------------------------------------------------------- +# Patch (optimistic-locking via ETag) +# --------------------------------------------------------------------------- + +async def initiate_patch( + db: AsyncSession, + report_id: uuid.UUID, + version_num: int, + user_id: uuid.UUID, + selected_text: str, + instruction: str, + if_match: str, +) -> ReportVersion: + """ + Validate a patch request and enqueue the patch job. + + Uses optimistic locking: the ``if_match`` ETag must match the + current version's content_hash. Returns 409 on conflict. + """ + version = await get_version_content(db, report_id, version_num, user_id) + + # Optimistic locking check + if version.content_hash != if_match: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Content has been modified since you last read it. Please refresh.", + ) + + # Verify selected_text exists in content (fuzzy match) + content = await load_content(version) + if selected_text not in content: + import difflib + ratio = difflib.SequenceMatcher(None, selected_text, content).quick_ratio() + if ratio < 0.85: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Selected text not found in report content", + ) + + return version # Caller enqueues the actual patch job with this context + + +async def delete_report( + db: AsyncSession, + report_id: uuid.UUID, + user_id: uuid.UUID, +) -> None: + """Soft-verify ownership and delete a report and all its versions.""" + report = await get_report(db, report_id, user_id) + await db.delete(report) + await db.commit() diff --git a/api/research/__init__.py b/api/research/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/research/router.py b/api/research/router.py new file mode 100644 index 0000000..1fe7c1b --- /dev/null +++ b/api/research/router.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +import json +import uuid +from typing import AsyncGenerator + +from arq import ArqRedis +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.responses import JSONResponse, StreamingResponse +from jose import JWTError, jwt +from sqlalchemy.ext.asyncio import AsyncSession + +from api.config import settings +from api.deps import get_current_user, get_db, get_redis +from api.research.schemas import CreateJobRequest, JobResponse +from api.research.service import cancel_job, create_job, get_job +from db.models import ResearchJob, User + +router = APIRouter(prefix="/research", tags=["research"]) + + +def _job_to_response(job: ResearchJob) -> JobResponse: + return JobResponse( + job_id=str(job.id), + report_id=str(job.report_id), + status=job.status, + current_phase=job.current_phase, + created_at=job.created_at, + started_at=job.started_at, + finished_at=job.finished_at, + error_detail=job.error_detail, + ) + + +@router.post("/jobs", response_model=JobResponse) +async def create_research_job( + body: CreateJobRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), + redis: ArqRedis = Depends(get_redis), +) -> JobResponse: + """ + Submit a new research job. + + Returns 201 on creation, 200 when the idempotency key matches an + existing job from the last 24 hours. + """ + job, is_new = await create_job( + db=db, + redis=redis, + user=current_user, + daily_token_budget=current_user.daily_token_budget, + request=body, + ) + response_data = _job_to_response(job) + http_status = status.HTTP_201_CREATED if is_new else status.HTTP_200_OK + return JSONResponse( + content=response_data.model_dump(mode="json"), + status_code=http_status, + ) + + +@router.get("/jobs/{job_id}", response_model=JobResponse) +async def get_research_job( + job_id: uuid.UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> JobResponse: + """Retrieve the current status of a research job.""" + job = await get_job(db, job_id, current_user.id) + return _job_to_response(job) + + +@router.get("/jobs/{job_id}/events") +async def stream_job_events( + job_id: uuid.UUID, + request: Request, + token: str | None = None, + db: AsyncSession = Depends(get_db), + redis: ArqRedis = Depends(get_redis), +) -> StreamingResponse: + """ + SSE stream for real-time job progress updates. + + Authentication: pass the short-lived SSE token (from GET /auth/sse-token) + as the `token` query parameter, since EventSource cannot set headers. + + Supports `Last-Event-ID` header for resuming after reconnect. + + Events include coarse ``job_status`` (phase rail) and append-only ``job_activity`` + (storyboard). Redis pub/sub is not durable: clients that connect after a job + finishes receive a terminal snapshot from the DB but not historical ``job_activity``. + """ + # Authenticate via short-lived SSE token or fall back to regular bearer + auth_header = request.headers.get("Authorization") + raw_token = token + if not raw_token and auth_header and auth_header.startswith("Bearer "): + raw_token = auth_header.split(" ", 1)[1] + + if not raw_token: + raise HTTPException(status_code=401, detail="SSE authentication required") + + try: + payload = jwt.decode(raw_token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) + user_id_str: str | None = payload.get("sub") + if not user_id_str: + raise JWTError("no sub") + except JWTError: + raise HTTPException(status_code=401, detail="Invalid SSE token") + + user_id = uuid.UUID(user_id_str) + + # Verify job ownership + job = await get_job(db, job_id, user_id) + + # Last-Event-ID is captured for future cursor-based resumption. + # Currently the SSE stream re-subscribes from the live position; + # historical event replay requires a persistent event log (future work). + _last_event_id = request.headers.get("Last-Event-ID") # noqa: F841 + + async def event_generator() -> AsyncGenerator[bytes, None]: + channel = f"job:{job_id}:events" + pubsub = redis.pubsub() + await pubsub.subscribe(channel) + + # If the job is already terminal, emit its final state immediately + current_job = job + if current_job.status in ("done", "failed", "cancelled"): + event_type = { + "done": "job_done", + "failed": "job_error", + "cancelled": "job_cancelled", + }.get(current_job.status, "job_status") + term_payload: dict = { + "status": current_job.status, + "current_phase": current_job.current_phase, + "error_detail": current_job.error_detail, + } + if current_job.started_at and current_job.finished_at: + term_payload["elapsed_ms"] = int( + (current_job.finished_at - current_job.started_at).total_seconds() + * 1000 + ) + data = json.dumps(term_payload) + yield f"event: {event_type}\ndata: {data}\n\n".encode() + await pubsub.unsubscribe(channel) + return + + try: + async for message in pubsub.listen(): + # Check if client disconnected + if await request.is_disconnected(): + break + + if message["type"] != "message": + continue + + raw = message["data"] + if isinstance(raw, bytes): + raw = raw.decode() + + try: + payload_data = json.loads(raw) + except (json.JSONDecodeError, ValueError): + continue + + event_type = payload_data.get("event", "job_status") + event_data = json.dumps(payload_data.get("data", {})) + event_id = payload_data.get("id", "") + + sse_line = f"id: {event_id}\nevent: {event_type}\ndata: {event_data}\n\n" + yield sse_line.encode() + + # Stop streaming once a terminal event is received + if event_type in ("job_done", "job_error", "job_cancelled"): + break + finally: + await pubsub.unsubscribe(channel) + await pubsub.close() + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) + + +@router.post("/jobs/{job_id}/cancel", status_code=status.HTTP_202_ACCEPTED) +async def cancel_research_job( + job_id: uuid.UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> dict: + """Request cancellation of a running or pending job.""" + job = await cancel_job(db, job_id, current_user.id) + return {"job_id": str(job.id), "status": job.status} diff --git a/api/research/schemas.py b/api/research/schemas.py new file mode 100644 index 0000000..8c37143 --- /dev/null +++ b/api/research/schemas.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + +from agents.chat.models import DEFAULT_MODEL_ID + + +class CreateJobRequest(BaseModel): + query: str = Field(..., min_length=10, max_length=2000) + strength: int = Field(2, ge=1, le=3, description="1=low, 2=medium, 3=high") + model_id: str = Field( + default=DEFAULT_MODEL_ID, + max_length=128, + description="Chat-catalog model id; full pipeline uses it with the user's BYOK key for that provider.", + ) + idempotency_key: Optional[str] = None + debug_mock: bool = Field( + default=False, + description="When true with server flag + allowlisted user, run mock worker (no LLM).", + ) + + +class JobResponse(BaseModel): + job_id: str + report_id: str + status: str + current_phase: Optional[str] + created_at: datetime + started_at: Optional[datetime] + finished_at: Optional[datetime] + error_detail: Optional[str] + + model_config = {"from_attributes": True} diff --git a/api/research/service.py b/api/research/service.py new file mode 100644 index 0000000..64f9560 --- /dev/null +++ b/api/research/service.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import uuid +from datetime import datetime, timedelta, timezone + +from arq import ArqRedis +from fastapi import HTTPException, status +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from api.config import settings +from api.debug_research_mock_policy import use_debug_mock_research_job +from api.research.schemas import CreateJobRequest +from db.models import Report, ResearchJob, UsageEvent, User + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +async def _check_daily_quota(db: AsyncSession, user_id: uuid.UUID, daily_token_budget: int) -> None: + """ + Sum prompt_tokens + completion_tokens from today's usage events. + Raises 429 if the user is over budget. + """ + today_start = _now().replace(hour=0, minute=0, second=0, microsecond=0) + result = await db.execute( + select( + func.coalesce(func.sum(UsageEvent.prompt_tokens), 0) + + func.coalesce(func.sum(UsageEvent.completion_tokens), 0) + ).where( + UsageEvent.user_id == user_id, + UsageEvent.created_at >= today_start, + ) + ) + tokens_used: int = result.scalar_one() or 0 + if tokens_used >= daily_token_budget: + raise HTTPException( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + detail="Daily token budget exhausted. Try again tomorrow.", + ) + + +async def _check_concurrent_limit(db: AsyncSession, user_id: uuid.UUID) -> None: + """Raise 429 if user already has max_concurrent_jobs_per_user running jobs.""" + result = await db.execute( + select(func.count(ResearchJob.id)).where( + ResearchJob.user_id == user_id, + ResearchJob.status.in_(["pending", "running"]), + ) + ) + active: int = result.scalar_one() or 0 + if active >= settings.max_concurrent_jobs_per_user: + raise HTTPException( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + detail=f"Maximum of {settings.max_concurrent_jobs_per_user} concurrent jobs allowed.", + ) + + +async def _find_idempotent_job( + db: AsyncSession, + user_id: uuid.UUID, + idempotency_key: str, +) -> ResearchJob | None: + """Return an existing job with this key created within the last 24 hours.""" + cutoff = _now() - timedelta(hours=24) + result = await db.execute( + select(ResearchJob).where( + ResearchJob.user_id == user_id, + ResearchJob.idempotency_key == idempotency_key, + ResearchJob.created_at >= cutoff, + ) + ) + return result.scalar_one_or_none() + + +async def create_job( + db: AsyncSession, + redis: ArqRedis, + user: User, + daily_token_budget: int, + request: CreateJobRequest, +) -> tuple[ResearchJob, bool]: + """ + Create a new research job (or return existing on idempotency hit). + + Returns (job, is_new) where is_new=False means the idempotency key matched + an existing job within the last 24 h. + """ + user_id = user.id + + # Idempotency check first — skip quota/concurrency check for duplicates + if request.idempotency_key: + existing = await _find_idempotent_job(db, user_id, request.idempotency_key) + if existing is not None: + return existing, False + + await _check_daily_quota(db, user_id, daily_token_budget) + await _check_concurrent_limit(db, user_id) + + from api.llm_credentials_service import ( + get_decrypted_provider_key, + model_provider, + validate_model_id, + ) + + job_model_id = validate_model_id(request.model_id) + + if not use_debug_mock_research_job(user, request.debug_mock): + prov = model_provider(job_model_id) + pk = await get_decrypted_provider_key(db, user_id, prov) + if not pk: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Add your {prov} API key in Profile → LLM keys to run research with this model.", + ) + + # Create report row + report = Report( + user_id=user_id, + query=request.query, + strength=request.strength, + ) + db.add(report) + await db.flush() # populate report.id + + # Create job row + job = ResearchJob( + report_id=report.id, + user_id=user_id, + idempotency_key=request.idempotency_key, + status="pending", + strength=request.strength, + llm_model_id=job_model_id, + attempts=0, + max_attempts=3, + created_at=_now(), + expires_at=_now() + timedelta(minutes=35), + ) + db.add(job) + await db.commit() + await db.refresh(job) + + if use_debug_mock_research_job(user, request.debug_mock): + await redis.enqueue_job("run_debug_mock_research_job", str(job.id)) + else: + await redis.enqueue_job("run_research_job", str(job.id)) + + return job, True + + +async def get_job( + db: AsyncSession, + job_id: uuid.UUID, + user_id: uuid.UUID, +) -> ResearchJob: + """Fetch a job with ownership check. Raises 404 if not found or wrong owner.""" + result = await db.execute( + select(ResearchJob).where( + ResearchJob.id == job_id, + ResearchJob.user_id == user_id, + ) + ) + job: ResearchJob | None = result.scalar_one_or_none() + if job is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Research job not found", + ) + return job + + +async def cancel_job( + db: AsyncSession, + job_id: uuid.UUID, + user_id: uuid.UUID, +) -> ResearchJob: + """ + Request cancellation of a pending or running job. + + Sets status to 'cancelled' if the job has not yet finished. + The worker checks expires_at / status so setting expires_at to now + acts as a cancellation signal when the job is already running. + """ + job = await get_job(db, job_id, user_id) + if job.status in ("done", "failed", "cancelled"): + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Job is already in terminal state: {job.status}", + ) + # Signal cancellation: set expires_at to now so the cancel token fires + job.status = "cancelled" + job.finished_at = _now() + await db.commit() + await db.refresh(job) + return job diff --git a/api/threads/debug_mock_research_stream.py b/api/threads/debug_mock_research_stream.py new file mode 100644 index 0000000..12ee90d --- /dev/null +++ b/api/threads/debug_mock_research_stream.py @@ -0,0 +1,106 @@ +""" +SSE byte chunks for debug mock chat research (no ChatAgent / run_pipeline). +""" +from __future__ import annotations + +import asyncio +import json +import uuid +from collections.abc import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession + +from api.threads.service import save_message + +_MOCK_ASSISTANT_MARKDOWN = """# Mock research response + +This answer is streamed from the **debug mock** path (no LLM). + +## Section + +- Item one (integration test) +- Item two + +### Math + +Inline: $E = mc^2$. + +> End of mock template. +""" + + +async def iter_debug_mock_research_sse( + db: AsyncSession, + thread_id: uuid.UUID, + research_strength: int, + user_query: str, +) -> AsyncGenerator[bytes, None]: + """ + Yields SSE lines: plan, step (several), token chunks, then persists assistant + message and yields done with message_id. + """ + plan = { + "mode": "research", + "reasoning": "Debug mock plan — no real thinker or pipeline.", + "selected_skills": ["web_search", "academic_search"], + "strength": research_strength, + "audience": "practitioner", + "steps": [ + { + "step_id": 1, + "type": "web_search", + "description": "Background scan (mock)", + "skill_name": None, + }, + { + "step_id": 2, + "type": "skill_call", + "description": "Academic sources (mock)", + "skill_name": "academic_search", + }, + { + "step_id": 3, + "type": "synthesis", + "description": "Merge findings (mock)", + "skill_name": None, + }, + ], + } + yield f"event: plan\ndata: {json.dumps(plan)}\n\n".encode() + + mock_steps = [ + (1, "web_search", "Resolving query in mock layer…"), + (2, "skill_call", "Consulting academic_search (mock)…"), + (3, "synthesis", "Streaming synthetic markdown…"), + ] + for step_id, step_type, desc in mock_steps: + await asyncio.sleep(0.08) + payload = json.dumps( + {"step_id": step_id, "step_type": step_type, "description": desc} + ) + yield f"event: step\ndata: {payload}\n\n".encode() + + text = ( + _MOCK_ASSISTANT_MARKDOWN + + "\n\n---\n\n*Debug mock — query excerpt:* " + + json.dumps(user_query[:300]) + + "\n" + ) + chunk_size = 48 + for i in range(0, len(text), chunk_size): + await asyncio.sleep(0.04) + piece = text[i : i + chunk_size] + yield f"event: token\ndata: {json.dumps({'token': piece})}\n\n".encode() + + msg = await save_message( + db, + thread_id, + "assistant", + text, + token_count=max(1, len(text) // 4), + ) + done_payload = { + "message_id": str(msg.id), + "token_count": msg.token_count, + } + yield f"event: done\ndata: {json.dumps(done_payload)}\n\n".encode() diff --git a/api/threads/router.py b/api/threads/router.py new file mode 100644 index 0000000..82b34f7 --- /dev/null +++ b/api/threads/router.py @@ -0,0 +1,241 @@ +"""Threads / Chat API router — conversation management and streaming responses.""" +from __future__ import annotations + +import json +import uuid +from typing import AsyncGenerator + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.responses import StreamingResponse +from sqlalchemy.ext.asyncio import AsyncSession + +from api.debug_research_mock_policy import assert_debug_mock_request_allowed +from api.deps import get_current_user, get_db +from api.llm_credentials_service import ( + require_provider_key_for_model, + validate_model_id, +) +from api.threads.schemas import ( + CreateThreadRequest, + MessageResponse, + PatchThreadRequest, + SendMessageRequest, + ThreadResponse, + ThreadSummaryResponse, + ThreadWithMessages, +) +from api.threads.debug_mock_research_stream import iter_debug_mock_research_sse +from api.threads.service import ( + assemble_context, + create_thread, + delete_thread, + get_messages, + get_thread, + list_threads_with_meta, + save_message, + update_thread_pin, +) +from db.models import User + +router = APIRouter(prefix="/threads", tags=["threads"]) + + +def _thread_to_response(t) -> ThreadResponse: + return ThreadResponse( + id=t.id, + report_id=t.report_id, + pinned_version_num=t.pinned_version_num, + canonical_report_qa=bool(getattr(t, "canonical_report_qa", False)), + created_at=t.created_at, + ) + + +def _message_to_response(m) -> MessageResponse: + return MessageResponse( + id=m.id, + role=m.role, + content=m.content, + token_count=m.token_count, + created_at=m.created_at, + ) + + +@router.post("", response_model=ThreadResponse, status_code=status.HTTP_201_CREATED) +async def create_new_thread( + body: CreateThreadRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> ThreadResponse: + """Create a new chat thread, optionally linked to a report.""" + thread = await create_thread( + db, + current_user.id, + body.report_id, + body.pinned_version, + ) + return _thread_to_response(thread) + + +@router.get("", response_model=list[ThreadSummaryResponse]) +async def list_user_threads( + limit: int = 50, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> list[ThreadSummaryResponse]: + """List the current user's threads (last activity first, with report title and preview).""" + rows = await list_threads_with_meta(db, current_user.id, limit) + out: list[ThreadSummaryResponse] = [] + for thread, report_title, preview, last_at, report_query, first_user_preview in rows: + base = _thread_to_response(thread) + out.append( + ThreadSummaryResponse( + **base.model_dump(), + report_title=report_title, + report_query=report_query, + last_message_at=last_at, + last_message_preview=preview, + first_user_message_preview=first_user_preview, + ) + ) + return out + + +@router.get("/{thread_id}", response_model=ThreadWithMessages) +async def get_thread_detail( + thread_id: uuid.UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> ThreadWithMessages: + """Get a thread with all its messages.""" + thread = await get_thread(db, thread_id, current_user.id) + messages = await get_messages(db, thread_id) + return ThreadWithMessages( + thread=_thread_to_response(thread), + messages=[_message_to_response(m) for m in messages], + ) + + +@router.patch("/{thread_id}", response_model=ThreadResponse) +async def patch_thread( + thread_id: uuid.UUID, + body: PatchThreadRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> ThreadResponse: + """Update thread settings (e.g. pinned report version for Q&A context).""" + thread = await update_thread_pin( + db, thread_id, current_user.id, body.pinned_version_num + ) + return _thread_to_response(thread) + + +@router.delete("/{thread_id}", status_code=status.HTTP_200_OK) +async def delete_user_thread( + thread_id: uuid.UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> dict: + """Delete a thread and all its messages.""" + await delete_thread(db, thread_id, current_user.id) + return {"status": "deleted"} + + +@router.post("/{thread_id}/messages") +async def send_message( + thread_id: uuid.UUID, + body: SendMessageRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> StreamingResponse: + """ + Send a message and receive a streaming response (SSE). + + The response streams events: plan, step_start, token, step_end, done, error. + """ + thread = await get_thread(db, thread_id, current_user.id) + + if body.debug_mock and body.execution_mode != "research": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="debug_mock is only valid when execution_mode is research", + ) + if body.debug_mock: + assert_debug_mock_request_allowed(current_user, True) + + # Save the user's message + await save_message(db, thread_id, "user", body.content) + + # Assemble context (history + report content if linked) + messages, _report_md = await assemble_context(db, thread) + + # Add the user's new message to the history + messages.append({"role": "user", "content": body.content}) + + async def event_generator() -> AsyncGenerator[bytes, None]: + assistant_content = "" + try: + if body.execution_mode == "research" and body.debug_mock: + async for chunk in iter_debug_mock_research_sse( + db, + thread_id, + body.research_strength, + body.content, + ): + yield chunk + return + + from agents.chat.agent import ChatAgent + + mid = validate_model_id(body.model_id) + api_key = await require_provider_key_for_model(db, current_user.id, mid) + agent = ChatAgent( + model_id=mid, + api_key=api_key, + ) + async for item in agent.stream_turn( + body.content, + messages[:-1], + execution_mode=body.execution_mode, + chat_variant=body.chat_variant, + research_strength=body.research_strength, + ): + if isinstance(item, dict) and item.get("kind") == "plan": + payload = json.dumps(item.get("plan", {})) + yield f"event: plan\ndata: {payload}\n\n".encode() + elif isinstance(item, dict) and item.get("kind") == "step": + payload = json.dumps({ + "step_id": item.get("step_id"), + "step_type": item.get("step_type"), + "description": item.get("description"), + }) + yield f"event: step\ndata: {payload}\n\n".encode() + elif isinstance(item, str): + assistant_content += item + data = json.dumps({"token": item}) + yield f"event: token\ndata: {data}\n\n".encode() + + if assistant_content: + saved = await save_message( + db, thread_id, "assistant", assistant_content + ) + done_payload = { + "message_id": str(saved.id), + "token_count": saved.token_count, + } + else: + done_payload = {} + yield f"event: done\ndata: {json.dumps(done_payload)}\n\n".encode() + + except Exception as e: + error_data = json.dumps({"message": str(e)}) + yield f"event: error\ndata: {error_data}\n\n".encode() + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) diff --git a/api/threads/schemas.py b/api/threads/schemas.py new file mode 100644 index 0000000..03df204 --- /dev/null +++ b/api/threads/schemas.py @@ -0,0 +1,72 @@ +"""Pydantic schemas for the Threads / Chat API.""" +from __future__ import annotations + +from datetime import datetime +from typing import Optional +from uuid import UUID + +from typing import Literal + +from pydantic import BaseModel, Field + +from agents.chat.models import DEFAULT_MODEL_ID + + +class CreateThreadRequest(BaseModel): + report_id: Optional[UUID] = None + pinned_version: Optional[int] = None + + +class ThreadResponse(BaseModel): + id: UUID + report_id: Optional[UUID] + pinned_version_num: Optional[int] + canonical_report_qa: bool = False + created_at: datetime + + model_config = {"from_attributes": True} + + +class ThreadSummaryResponse(ThreadResponse): + """Thread row for chat history lists (activity sort + report context).""" + + report_title: Optional[str] = None + report_query: Optional[str] = None + last_message_at: Optional[datetime] = None + last_message_preview: Optional[str] = None + first_user_message_preview: Optional[str] = None + + +class PatchThreadRequest(BaseModel): + """Pin a specific report version for context, or null to follow latest.""" + + pinned_version_num: Optional[int] = None + + +class MessageResponse(BaseModel): + id: UUID + role: str + content: str + token_count: Optional[int] + created_at: datetime + + model_config = {"from_attributes": True} + + +class ThreadWithMessages(BaseModel): + thread: ThreadResponse + messages: list[MessageResponse] + + +class SendMessageRequest(BaseModel): + content: str = Field(..., min_length=1, max_length=10000) + execution_mode: Literal["chat", "research"] = "chat" + """User-selected run mode: lightweight chat steps vs full research pipeline.""" + chat_variant: Literal["standard", "extended"] = "standard" + """When execution_mode is chat: standard (1–5 style steps) vs extended thinker (deeper plans).""" + research_strength: int = Field(2, ge=1, le=3) + """When execution_mode is research: intensity tier (1=low, 2=medium, 3=high) for run_pipeline.""" + model_id: str = Field(default=DEFAULT_MODEL_ID, max_length=160) + """Chat-catalog model id; thinker, chat execution, and research pipeline all use this model with BYOK.""" + debug_mock: bool = False + """With server flag + allowlisted user: stream mock plan/steps/tokens (research only).""" diff --git a/api/threads/service.py b/api/threads/service.py new file mode 100644 index 0000000..da4065e --- /dev/null +++ b/api/threads/service.py @@ -0,0 +1,345 @@ +""" +Thread service layer. + +Manages conversation threads (both standalone chat and report-linked Q&A), +message persistence, and context assembly for the chat agent. +""" +from __future__ import annotations + +import uuid +from datetime import datetime, timezone +from typing import Optional + +from fastapi import HTTPException, status +from sqlalchemy import func, select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession + +from api.reports.service import get_report, get_version_content, load_content +from db.models import Message, Report, Thread, User + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +async def create_thread( + db: AsyncSession, + user_id: uuid.UUID, + report_id: Optional[uuid.UUID], + pinned_version: Optional[int], +) -> Thread: + """ + Create a new chat thread, optionally linked to a report. + """ + if report_id: + await get_report(db, report_id, user_id) + + thread = Thread( + user_id=user_id, + report_id=report_id, + pinned_version_num=pinned_version, + canonical_report_qa=False, + created_at=_now(), + ) + db.add(thread) + await db.commit() + await db.refresh(thread) + return thread + + +async def get_thread( + db: AsyncSession, + thread_id: uuid.UUID, + user_id: uuid.UUID, +) -> Thread: + """Fetch a thread with ownership check.""" + result = await db.execute( + select(Thread).where(Thread.id == thread_id, Thread.user_id == user_id) + ) + thread: Thread | None = result.scalar_one_or_none() + if thread is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Thread not found") + return thread + + +async def list_threads( + db: AsyncSession, + user_id: uuid.UUID, + limit: int = 20, +) -> list[Thread]: + """List user's threads, newest first (created_at only — prefer list_threads_with_meta for UI).""" + result = await db.execute( + select(Thread) + .where(Thread.user_id == user_id) + .order_by(Thread.created_at.desc()) + .limit(limit) + ) + return list(result.scalars().all()) + + +async def get_or_create_default_report_thread( + db: AsyncSession, + user_id: uuid.UUID, + report_id: uuid.UUID, +) -> Thread: + """ + Return the single canonical Q&A thread for this report, creating it if absent. + + Concurrent creates resolve via unique index + IntegrityError retry. + """ + await get_report(db, report_id, user_id) + result = await db.execute( + select(Thread).where( + Thread.user_id == user_id, + Thread.report_id == report_id, + Thread.canonical_report_qa.is_(True), + ) + ) + existing: Thread | None = result.scalar_one_or_none() + if existing is not None: + return existing + + thread = Thread( + user_id=user_id, + report_id=report_id, + pinned_version_num=None, + canonical_report_qa=True, + created_at=_now(), + ) + db.add(thread) + try: + await db.commit() + await db.refresh(thread) + return thread + except IntegrityError: + await db.rollback() + result = await db.execute( + select(Thread).where( + Thread.user_id == user_id, + Thread.report_id == report_id, + Thread.canonical_report_qa.is_(True), + ) + ) + retry = result.scalar_one_or_none() + if retry is None: + raise + return retry + + +async def list_threads_with_meta( + db: AsyncSession, + user_id: uuid.UUID, + limit: int = 50, +) -> list[ + tuple[ + Thread, + Optional[str], + Optional[str], + Optional[datetime], + Optional[str], + Optional[str], + ] +]: + """ + List threads with report title/query and message previews, ordered by last activity. + + At most one row per report: prefers ``canonical_report_qa`` then latest activity + (avoids duplicate sidebar rows from legacy extra threads per report). + + Returns tuples: + (thread, report_title, last_message_preview, last_message_at, report_query, + first_user_message_preview). + """ + last_at_sub = ( + select( + Message.thread_id.label("tid"), + func.max(Message.created_at).label("last_at"), + ) + .group_by(Message.thread_id) + .subquery() + ) + + sort_key_expr = func.coalesce(last_at_sub.c.last_at, Thread.created_at) + ranked_sq = ( + select( + Thread.id.label("picked_thread_id"), + sort_key_expr.label("sort_key"), + func.row_number() + .over( + partition_by=func.coalesce(Thread.report_id, Thread.id), + order_by=( + Thread.canonical_report_qa.desc(), + sort_key_expr.desc(), + ), + ) + .label("rn"), + ) + .select_from(Thread) + .outerjoin(last_at_sub, Thread.id == last_at_sub.c.tid) + .where(Thread.user_id == user_id) + ).subquery() + + stmt = ( + select(Thread, Report.title, last_at_sub.c.last_at, Report.query) + .join(ranked_sq, Thread.id == ranked_sq.c.picked_thread_id) + .outerjoin(Report, Thread.report_id == Report.id) + .outerjoin(last_at_sub, Thread.id == last_at_sub.c.tid) + .where(ranked_sq.c.rn == 1) + .order_by(ranked_sq.c.sort_key.desc()) + .limit(limit) + ) + rows = list((await db.execute(stmt)).all()) + thread_ids = [row[0].id for row in rows] + preview_by_tid: dict[uuid.UUID, str] = {} + first_user_by_tid: dict[uuid.UUID, str] = {} + if thread_ids: + rn = func.row_number().over( + partition_by=Message.thread_id, + order_by=Message.created_at.desc(), + ).label("rn") + ranked = ( + select(Message.thread_id, Message.content, rn) + .where(Message.thread_id.in_(thread_ids)) + .subquery() + ) + prev_stmt = select(ranked.c.thread_id, ranked.c.content).where(ranked.c.rn == 1) + for tid, content in (await db.execute(prev_stmt)).all(): + text = (content or "").strip().replace("\n", " ") + preview_by_tid[tid] = text[:120] + ("…" if len(text) > 120 else "") + + rn_first = func.row_number().over( + partition_by=Message.thread_id, + order_by=Message.created_at.asc(), + ).label("rn_asc") + ranked_first = ( + select(Message.thread_id, Message.content, rn_first) + .where( + Message.thread_id.in_(thread_ids), + Message.role == "user", + ) + .subquery() + ) + first_stmt = select(ranked_first.c.thread_id, ranked_first.c.content).where( + ranked_first.c.rn_asc == 1 + ) + for tid, content in (await db.execute(first_stmt)).all(): + text = (content or "").strip().replace("\n", " ") + first_user_by_tid[tid] = text[:120] + ("…" if len(text) > 120 else "") + + out: list[ + tuple[ + Thread, + Optional[str], + Optional[str], + Optional[datetime], + Optional[str], + Optional[str], + ] + ] = [] + for thread, title, last_at, report_query in rows: + preview = preview_by_tid.get(thread.id) + first_user = first_user_by_tid.get(thread.id) + out.append((thread, title, preview, last_at, report_query, first_user)) + return out + + +async def update_thread_pin( + db: AsyncSession, + thread_id: uuid.UUID, + user_id: uuid.UUID, + pinned_version_num: Optional[int], +) -> Thread: + """Set pinned report version for a thread (None = follow latest).""" + thread = await get_thread(db, thread_id, user_id) + thread.pinned_version_num = pinned_version_num + await db.commit() + await db.refresh(thread) + return thread + + +async def save_message( + db: AsyncSession, + thread_id: uuid.UUID, + role: str, + content: str, + token_count: Optional[int] = None, +) -> Message: + """Persist a chat message.""" + message = Message( + thread_id=thread_id, + role=role, + content=content, + token_count=token_count, + created_at=_now(), + ) + db.add(message) + await db.commit() + await db.refresh(message) + return message + + +async def get_messages( + db: AsyncSession, + thread_id: uuid.UUID, + limit: int = 100, +) -> list[Message]: + """Get messages for a thread, oldest first.""" + result = await db.execute( + select(Message) + .where(Message.thread_id == thread_id) + .order_by(Message.created_at.asc()) + .limit(limit) + ) + return list(result.scalars().all()) + + +async def assemble_context( + db: AsyncSession, + thread: Thread, +) -> tuple[list[dict], Optional[str]]: + """ + Build the conversation context for the chat agent. + + Returns: + - messages: list of {"role": ..., "content": ...} for the agent + - report_md: the report markdown if linked, else None + """ + # Load message history + db_messages = await get_messages(db, thread.id) + messages = [ + {"role": m.role, "content": m.content} + for m in db_messages + ] + + # Load linked report content if available + report_md: Optional[str] = None + if thread.report_id: + version_num = thread.pinned_version_num + if version_num: + try: + version = await get_version_content( + db, thread.report_id, version_num, thread.user_id, + ) + report_md = await load_content(version) + except HTTPException: + pass # Version may have been deleted; continue without context + else: + # Load latest version + from api.reports.service import get_latest_version + latest = await get_latest_version(db, thread.report_id) + if latest: + report_md = await load_content(latest) + + return messages, report_md + + +async def delete_thread( + db: AsyncSession, + thread_id: uuid.UUID, + user_id: uuid.UUID, +) -> None: + """Delete a thread and all its messages.""" + thread = await get_thread(db, thread_id, user_id) + await db.delete(thread) + await db.commit() diff --git a/api/users/router.py b/api/users/router.py new file mode 100644 index 0000000..9baa181 --- /dev/null +++ b/api/users/router.py @@ -0,0 +1,147 @@ +"""Users / Stats API router — profile, usage, analytics.""" +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.ext.asyncio import AsyncSession + +from api.deps import get_current_user, get_db +from api.llm_credentials_service import ( + credential_row_to_public, + delete_credential, + list_credentials_public, + normalize_provider, + upsert_credential, +) +from api.users.schemas import ( + DeviceBreakdownResponse, + LlmCredentialDeleteResponse, + LlmCredentialListResponse, + LlmCredentialPublic, + LlmCredentialPutBody, + ModelBreakdownResponse, + UsageSeriesResponse, + UsageStats, + UserProfile, +) +from api.users.service import ( + compute_stats, + get_device_breakdown, + get_model_breakdown, + get_usage_series, + get_user_profile, +) +from db.models import User + +router = APIRouter(prefix="/users", tags=["users"]) + + +@router.get("/me", response_model=UserProfile) +async def get_me( + current_user: User = Depends(get_current_user), +) -> UserProfile: + """Return the current authenticated user's profile.""" + return UserProfile( + id=current_user.id, + email=current_user.email, + name=current_user.name, + avatar_url=current_user.avatar_url, + created_at=current_user.created_at, + daily_token_budget=current_user.daily_token_budget, + ) + + +@router.get("/me/stats", response_model=UsageStats) +async def get_usage_stats( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> UsageStats: + """Aggregate usage statistics for the current user.""" + stats = await compute_stats(db, current_user.id) + return UsageStats(**stats) + + +@router.get("/me/usage", response_model=UsageSeriesResponse) +async def get_usage( + range: str = Query("30d", regex="^(7d|30d|90d)$"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> UsageSeriesResponse: + """ + Daily usage time series for graphs. + + ``range`` can be 7d, 30d, or 90d. + """ + days_map = {"7d": 7, "30d": 30, "90d": 90} + days = days_map[range] + data = await get_usage_series(db, current_user.id, days) + return UsageSeriesResponse(**data) + + +@router.get("/me/usage/models", response_model=ModelBreakdownResponse) +async def get_models( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> ModelBreakdownResponse: + """Token and cost breakdown by LLM model.""" + data = await get_model_breakdown(db, current_user.id) + return ModelBreakdownResponse(**data) + + +@router.get("/me/usage/devices", response_model=DeviceBreakdownResponse) +async def get_devices( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> DeviceBreakdownResponse: + """Device, OS, and browser breakdown from usage events.""" + data = await get_device_breakdown(db, current_user.id) + return DeviceBreakdownResponse(**data) + + +@router.get("/me/llm-credentials", response_model=LlmCredentialListResponse) +async def list_llm_credentials( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> LlmCredentialListResponse: + rows = await list_credentials_public(db, current_user.id) + return LlmCredentialListResponse( + credentials=[LlmCredentialPublic(**r) for r in rows] + ) + + +@router.put("/me/llm-credentials/{provider}", response_model=LlmCredentialPublic) +async def put_llm_credential( + provider: str, + body: LlmCredentialPutBody, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> LlmCredentialPublic: + prov = normalize_provider(provider) + row = await upsert_credential( + db, current_user.id, prov, body.secret, body.label + ) + return LlmCredentialPublic(**credential_row_to_public(row)) + + +@router.delete( + "/me/llm-credentials/{provider}", + response_model=LlmCredentialDeleteResponse, + status_code=status.HTTP_200_OK, +) +async def remove_llm_credential( + provider: str, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> LlmCredentialDeleteResponse: + """ + Remove the current user's encrypted API key for ``provider`` (grok | gemini | deepseek). + + Returns 404 if no credential exists for that provider. + """ + prov = normalize_provider(provider) + ok = await delete_credential(db, current_user.id, prov) + if not ok: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No API key saved for this provider.", + ) + return LlmCredentialDeleteResponse() diff --git a/api/users/schemas.py b/api/users/schemas.py new file mode 100644 index 0000000..923fdda --- /dev/null +++ b/api/users/schemas.py @@ -0,0 +1,102 @@ +"""Pydantic schemas for the Users / Stats API.""" +from __future__ import annotations + +from datetime import datetime +from decimal import Decimal +from typing import Optional +from uuid import UUID + +from pydantic import BaseModel, Field + + +class UserProfile(BaseModel): + id: UUID + email: str + name: Optional[str] + avatar_url: Optional[str] + created_at: datetime + daily_token_budget: int + + model_config = {"from_attributes": True} + + +class UsageStats(BaseModel): + total_reports: int + total_tokens: int + total_cost_usd: Decimal + reports_this_week: int + tokens_today: int + tokens_remaining_today: int + streak_days: int + avg_report_strength: float + favorite_model: Optional[str] + most_active_hour: Optional[int] + + +class UsageSeriesPoint(BaseModel): + date: str + tokens: int + cost_usd: Decimal + reports: int + + +class UsageSeriesResponse(BaseModel): + series: list[UsageSeriesPoint] + total_tokens: int + total_cost_usd: Decimal + + +class ModelBreakdownItem(BaseModel): + model: str + tokens: int + cost_usd: Decimal + pct: int + + +class ModelBreakdownResponse(BaseModel): + breakdown: list[ModelBreakdownItem] + + +class DeviceBreakdownItem(BaseModel): + device_type: str + count: int + + +class OSBreakdownItem(BaseModel): + os: str + count: int + + +class BrowserBreakdownItem(BaseModel): + browser: str + count: int + + +class DeviceBreakdownResponse(BaseModel): + devices: list[DeviceBreakdownItem] + os: list[OSBreakdownItem] + browsers: list[BrowserBreakdownItem] + + +class LlmCredentialPublic(BaseModel): + id: UUID + provider: str + label: Optional[str] + last_four: str + created_at: datetime + updated_at: datetime + + +class LlmCredentialListResponse(BaseModel): + credentials: list[LlmCredentialPublic] + + +class LlmCredentialPutBody(BaseModel): + secret: str = Field(..., min_length=8) + label: Optional[str] = Field(None, max_length=128) + + +class LlmCredentialDeleteResponse(BaseModel): + """Returned after removing a stored provider key (BYOK).""" + + status: str = Field("deleted", description="Always 'deleted' on success.") diff --git a/api/users/service.py b/api/users/service.py new file mode 100644 index 0000000..405b638 --- /dev/null +++ b/api/users/service.py @@ -0,0 +1,283 @@ +""" +User service layer — profile, stats, and usage analytics. +""" +from __future__ import annotations + +import uuid +from datetime import datetime, timedelta, timezone +from decimal import Decimal +from typing import Optional + +from fastapi import HTTPException, status +from sqlalchemy import func, select, text +from sqlalchemy.ext.asyncio import AsyncSession + +from api.config import settings +from db.models import Report, UsageEvent, User + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +async def get_user_profile(db: AsyncSession, user_id: uuid.UUID) -> User: + result = await db.execute(select(User).where(User.id == user_id)) + user: User | None = result.scalar_one_or_none() + if user is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + return user + + +async def compute_stats(db: AsyncSession, user_id: uuid.UUID) -> dict: + """Compute aggregate usage statistics for the user.""" + now = _now() + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + week_start = now - timedelta(days=now.weekday()) + week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0) + + # Total reports + total_reports_result = await db.execute( + select(func.count(Report.id)).where(Report.user_id == user_id) + ) + total_reports: int = total_reports_result.scalar() or 0 + + # Reports this week + reports_week_result = await db.execute( + select(func.count(Report.id)).where( + Report.user_id == user_id, + Report.created_at >= week_start, + ) + ) + reports_this_week: int = reports_week_result.scalar() or 0 + + # Token usage (prompt + completion) today + tokens_today_result = await db.execute( + select( + func.coalesce(func.sum(UsageEvent.prompt_tokens), 0) + + func.coalesce(func.sum(UsageEvent.completion_tokens), 0) + ).where( + UsageEvent.user_id == user_id, + UsageEvent.created_at >= today_start, + ) + ) + tokens_today: int = int(tokens_today_result.scalar() or 0) + + # Total tokens and cost + totals_result = await db.execute( + select( + func.coalesce(func.sum(UsageEvent.prompt_tokens), 0) + + func.coalesce(func.sum(UsageEvent.completion_tokens), 0), + func.coalesce(func.sum(UsageEvent.cost_usd), 0), + ).where(UsageEvent.user_id == user_id) + ) + row = totals_result.one() + total_tokens: int = int(row[0] or 0) + total_cost_usd: Decimal = row[1] or Decimal("0") + + # Average report strength + avg_strength_result = await db.execute( + select(func.avg(Report.strength)).where(Report.user_id == user_id) + ) + avg_strength: float = float(avg_strength_result.scalar() or 2.0) + + # Favorite model (most tokens used) + fav_model_result = await db.execute( + select(UsageEvent.model, func.sum(UsageEvent.prompt_tokens + UsageEvent.completion_tokens).label("total")) + .where(UsageEvent.user_id == user_id, UsageEvent.model.isnot(None)) + .group_by(UsageEvent.model) + .order_by(text("total DESC")) + .limit(1) + ) + fav_row = fav_model_result.first() + favorite_model: Optional[str] = fav_row[0] if fav_row else None + + # Most active hour + active_hour_result = await db.execute( + select( + func.extract("hour", UsageEvent.created_at).label("h"), + func.count(UsageEvent.id).label("cnt"), + ) + .where(UsageEvent.user_id == user_id) + .group_by(text("h")) + .order_by(text("cnt DESC")) + .limit(1) + ) + hour_row = active_hour_result.first() + most_active_hour: Optional[int] = int(hour_row[0]) if hour_row else None + + # Streak: count consecutive days with usage ending today + streak = await _compute_streak(db, user_id) + + # Get user's daily budget + user_result = await db.execute(select(User).where(User.id == user_id)) + user: User | None = user_result.scalar_one_or_none() + daily_budget = user.daily_token_budget if user else settings.default_daily_token_budget + + return { + "total_reports": total_reports, + "total_tokens": total_tokens, + "total_cost_usd": total_cost_usd, + "reports_this_week": reports_this_week, + "tokens_today": tokens_today, + "tokens_remaining_today": max(0, daily_budget - tokens_today), + "streak_days": streak, + "avg_report_strength": round(avg_strength, 1), + "favorite_model": favorite_model, + "most_active_hour": most_active_hour, + } + + +async def _compute_streak(db: AsyncSession, user_id: uuid.UUID) -> int: + """Compute the current daily usage streak.""" + now = _now() + streak = 0 + for days_ago in range(365): # Max 1 year streak + day = now - timedelta(days=days_ago) + day_start = day.replace(hour=0, minute=0, second=0, microsecond=0) + day_end = day_start + timedelta(days=1) + + result = await db.execute( + select(func.count(UsageEvent.id)).where( + UsageEvent.user_id == user_id, + UsageEvent.created_at >= day_start, + UsageEvent.created_at < day_end, + ) + ) + count: int = result.scalar() or 0 + if count > 0: + streak += 1 + else: + # Allow today to have no activity yet + if days_ago == 0: + continue + break + return streak + + +async def get_usage_series( + db: AsyncSession, + user_id: uuid.UUID, + days: int = 30, +) -> dict: + """Get daily usage time series.""" + now = _now() + start = now - timedelta(days=days) + start_date = start.replace(hour=0, minute=0, second=0, microsecond=0) + + result = await db.execute( + select( + func.date_trunc("day", UsageEvent.created_at).label("day"), + func.sum( + func.coalesce(UsageEvent.prompt_tokens, 0) + + func.coalesce(UsageEvent.completion_tokens, 0) + ).label("tokens"), + func.sum(UsageEvent.cost_usd).label("cost"), + ) + .where( + UsageEvent.user_id == user_id, + UsageEvent.created_at >= start_date, + ) + .group_by(text("day")) + .order_by(text("day")) + ) + rows = result.all() + + # Count reports per day + report_result = await db.execute( + select( + func.date_trunc("day", Report.created_at).label("day"), + func.count(Report.id).label("count"), + ) + .where( + Report.user_id == user_id, + Report.created_at >= start_date, + ) + .group_by(text("day")) + ) + report_counts = {r[0].strftime("%Y-%m-%d"): r[1] for r in report_result.all()} + + series = [] + total_tokens = 0 + total_cost = Decimal("0") + for row in rows: + date_str = row[0].strftime("%Y-%m-%d") + tokens = int(row[1] or 0) + cost = row[2] or Decimal("0") + total_tokens += tokens + total_cost += cost + series.append({ + "date": date_str, + "tokens": tokens, + "cost_usd": cost, + "reports": report_counts.get(date_str, 0), + }) + + return { + "series": series, + "total_tokens": total_tokens, + "total_cost_usd": total_cost, + } + + +async def get_model_breakdown(db: AsyncSession, user_id: uuid.UUID) -> dict: + """Get token and cost breakdown by model.""" + result = await db.execute( + select( + UsageEvent.model, + func.sum( + func.coalesce(UsageEvent.prompt_tokens, 0) + + func.coalesce(UsageEvent.completion_tokens, 0) + ).label("tokens"), + func.sum(UsageEvent.cost_usd).label("cost"), + ) + .where(UsageEvent.user_id == user_id, UsageEvent.model.isnot(None)) + .group_by(UsageEvent.model) + .order_by(text("tokens DESC")) + ) + rows = result.all() + + total_tokens = sum(int(r[1] or 0) for r in rows) + breakdown = [] + for row in rows: + model_tokens = int(row[1] or 0) + pct = int((model_tokens / total_tokens * 100) if total_tokens > 0 else 0) + breakdown.append({ + "model": row[0], + "tokens": model_tokens, + "cost_usd": row[2] or Decimal("0"), + "pct": pct, + }) + + return {"breakdown": breakdown} + + +async def get_device_breakdown(db: AsyncSession, user_id: uuid.UUID) -> dict: + """Get device, OS, and browser breakdown.""" + # Devices + device_result = await db.execute( + select(UsageEvent.device_type, func.count(UsageEvent.id).label("count")) + .where(UsageEvent.user_id == user_id, UsageEvent.device_type.isnot(None)) + .group_by(UsageEvent.device_type) + .order_by(text("count DESC")) + ) + devices = [{"device_type": r[0], "count": r[1]} for r in device_result.all()] + + # OS + os_result = await db.execute( + select(UsageEvent.os, func.count(UsageEvent.id).label("count")) + .where(UsageEvent.user_id == user_id, UsageEvent.os.isnot(None)) + .group_by(UsageEvent.os) + .order_by(text("count DESC")) + ) + os_list = [{"os": r[0], "count": r[1]} for r in os_result.all()] + + # Browsers + browser_result = await db.execute( + select(UsageEvent.browser, func.count(UsageEvent.id).label("count")) + .where(UsageEvent.user_id == user_id, UsageEvent.browser.isnot(None)) + .group_by(UsageEvent.browser) + .order_by(text("count DESC")) + ) + browsers = [{"browser": r[0], "count": r[1]} for r in browser_result.all()] + + return {"devices": devices, "os": os_list, "browsers": browsers} diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/db/migrations/__init__.py b/db/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/db/migrations/env.py b/db/migrations/env.py new file mode 100644 index 0000000..32885df --- /dev/null +++ b/db/migrations/env.py @@ -0,0 +1,88 @@ +import asyncio +import os +from logging.config import fileConfig + +from alembic import context +from dotenv import load_dotenv +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +# Load .env so DATABASE_URL is available when running alembic outside Docker +load_dotenv() + +# This is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# Override sqlalchemy.url from environment — never bake credentials into alembic.ini +database_url = os.environ["DATABASE_URL"] +config.set_main_option("sqlalchemy.url", database_url) + +# Import all models here so that autogenerate can detect schema changes. +# Add new model modules here as they are created. +from db import models # noqa: F401, E402 + +target_metadata = models.Base.metadata + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL and not an Engine. + Calls to context.execute() emit the given string to the script output. + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + compare_server_default=True, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True, + compare_server_default=True, + ) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """Run migrations in 'online' mode using an async engine.""" + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Entry point for online mode — delegates to async runner.""" + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/db/migrations/versions/.gitkeep b/db/migrations/versions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/db/migrations/versions/0001_initial.py b/db/migrations/versions/0001_initial.py new file mode 100644 index 0000000..25638af --- /dev/null +++ b/db/migrations/versions/0001_initial.py @@ -0,0 +1,166 @@ +"""initial schema + +Revision ID: 0001_initial +Revises: None +Create Date: 2026-04-01 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '0001_initial' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Users + op.create_table( + 'users', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('google_sub', sa.String(), nullable=False, unique=True), + sa.Column('email', sa.String(), nullable=False, unique=True), + sa.Column('name', sa.String(), nullable=True), + sa.Column('avatar_url', sa.String(), nullable=True), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column('last_login_at', sa.TIMESTAMP(timezone=True), nullable=True), + sa.Column('daily_token_budget', sa.Integer(), nullable=False, server_default='1000000'), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'), + ) + op.create_index('ix_users_google_sub', 'users', ['google_sub']) + + # Refresh Tokens + op.create_table( + 'refresh_tokens', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False), + sa.Column('token_hash', sa.String(), nullable=False, unique=True), + sa.Column('family_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column('expires_at', sa.TIMESTAMP(timezone=True), nullable=False), + sa.Column('revoked_at', sa.TIMESTAMP(timezone=True), nullable=True), + ) + op.create_index('idx_rt_user_id', 'refresh_tokens', ['user_id']) + + # Reports + op.create_table( + 'reports', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False), + sa.Column('title', sa.String(), nullable=True), + sa.Column('query', sa.Text(), nullable=False), + sa.Column('strength', sa.SmallInteger(), nullable=False, server_default='5'), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + ) + op.create_index('idx_reports_user_id', 'reports', ['user_id', 'created_at']) + + # Report Versions + op.create_table( + 'report_versions', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('report_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('reports.id', ondelete='CASCADE'), nullable=False), + sa.Column('version_num', sa.Integer(), nullable=False), + sa.Column('content_inline', sa.Text(), nullable=True), + sa.Column('content_uri', sa.String(), nullable=True), + sa.Column('content_hash', sa.String(), nullable=False), + sa.Column('char_count', sa.Integer(), nullable=False), + sa.Column('patch_instruction', sa.Text(), nullable=True), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + sa.UniqueConstraint('report_id', 'version_num', name='uq_report_version'), + ) + op.create_index('idx_rv_report_id', 'report_versions', ['report_id', 'version_num']) + + # Research Jobs + op.create_table( + 'research_jobs', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('report_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('reports.id', ondelete='CASCADE'), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False), + sa.Column('idempotency_key', sa.String(), nullable=True), + sa.Column('status', sa.String(), nullable=False, server_default='pending'), + sa.Column('strength', sa.SmallInteger(), nullable=False, server_default='5'), + sa.Column('attempts', sa.SmallInteger(), nullable=False, server_default='0'), + sa.Column('max_attempts', sa.SmallInteger(), nullable=False, server_default='3'), + sa.Column('current_phase', sa.String(), nullable=True), + sa.Column('error_detail', sa.Text(), nullable=True), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column('started_at', sa.TIMESTAMP(timezone=True), nullable=True), + sa.Column('finished_at', sa.TIMESTAMP(timezone=True), nullable=True), + sa.Column('expires_at', sa.TIMESTAMP(timezone=True), nullable=True), + ) + op.create_index( + 'idx_rj_idempotency', 'research_jobs', ['user_id', 'idempotency_key'], + unique=True, + postgresql_where='idempotency_key IS NOT NULL', + ) + op.create_index('idx_rj_user_status', 'research_jobs', ['user_id', 'status']) + + # Threads + op.create_table( + 'threads', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('report_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('reports.id', ondelete='CASCADE'), nullable=True), + sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False), + sa.Column('pinned_version_num', sa.Integer(), nullable=True), + sa.Column('summary', sa.Text(), nullable=True), + sa.Column('summary_through_message_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + ) + op.create_index('idx_threads_user_id', 'threads', ['user_id', 'created_at']) + + # Messages + op.create_table( + 'messages', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('thread_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('threads.id', ondelete='CASCADE'), nullable=False), + sa.Column('role', sa.String(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('token_count', sa.Integer(), nullable=True), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + ) + op.create_index('idx_messages_thread', 'messages', ['thread_id', 'created_at']) + + # Usage Events + op.create_table( + 'usage_events', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False), + sa.Column('session_id', sa.String(), nullable=True), + sa.Column('event_type', sa.String(), nullable=False), + sa.Column('model', sa.String(), nullable=True), + sa.Column('prompt_tokens', sa.Integer(), nullable=True), + sa.Column('completion_tokens', sa.Integer(), nullable=True), + sa.Column('cost_usd', sa.Numeric(10, 6), nullable=True), + sa.Column('route', sa.String(), nullable=True), + sa.Column('report_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('job_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('thread_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('duration_ms', sa.Integer(), nullable=True), + sa.Column('success', sa.Boolean(), nullable=True), + sa.Column('error_code', sa.String(), nullable=True), + sa.Column('user_agent', sa.Text(), nullable=True), + sa.Column('ip_address', sa.String(45), nullable=True), + sa.Column('device_type', sa.String(), nullable=True), + sa.Column('os', sa.String(), nullable=True), + sa.Column('browser', sa.String(), nullable=True), + sa.Column('country', sa.String(), nullable=True), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + ) + op.create_index('idx_ue_user_day', 'usage_events', ['user_id', 'created_at']) + op.create_index('idx_ue_event_type', 'usage_events', ['user_id', 'event_type', 'created_at']) + + +def downgrade() -> None: + op.drop_table('usage_events') + op.drop_table('messages') + op.drop_table('threads') + op.drop_table('research_jobs') + op.drop_table('report_versions') + op.drop_table('reports') + op.drop_table('refresh_tokens') + op.drop_table('users') diff --git a/db/migrations/versions/0002_thread_canonical_report_qa.py b/db/migrations/versions/0002_thread_canonical_report_qa.py new file mode 100644 index 0000000..f62ac0b --- /dev/null +++ b/db/migrations/versions/0002_thread_canonical_report_qa.py @@ -0,0 +1,40 @@ +"""thread canonical_report_qa for default report Q&A thread + +Revision ID: 0002_thread_canonical +Revises: 0001_initial +Create Date: 2026-04-02 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +revision: str = "0002_thread_canonical" +down_revision: Union[str, None] = "0001_initial" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "threads", + sa.Column( + "canonical_report_qa", + sa.Boolean(), + nullable=False, + server_default="false", + ), + ) + op.create_index( + "uq_threads_user_report_canonical", + "threads", + ["user_id", "report_id"], + unique=True, + postgresql_where=sa.text("report_id IS NOT NULL AND canonical_report_qa IS TRUE"), + ) + + +def downgrade() -> None: + op.drop_index("uq_threads_user_report_canonical", table_name="threads") + op.drop_column("threads", "canonical_report_qa") diff --git a/db/migrations/versions/0003_user_llm_credentials.py b/db/migrations/versions/0003_user_llm_credentials.py new file mode 100644 index 0000000..19899f8 --- /dev/null +++ b/db/migrations/versions/0003_user_llm_credentials.py @@ -0,0 +1,37 @@ +"""user_llm_credentials BYOK + +Revision ID: 0003_user_llm +Revises: 0002_thread_canonical +Create Date: 2026-04-02 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + +revision: str = "0003_user_llm" +down_revision: Union[str, None] = "0002_thread_canonical" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "user_llm_credentials", + sa.Column("id", UUID(as_uuid=True), primary_key=True, nullable=False), + sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), + sa.Column("provider", sa.String(32), nullable=False), + sa.Column("encrypted_secret", sa.Text(), nullable=False), + sa.Column("label", sa.String(128), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.UniqueConstraint("user_id", "provider", name="uq_user_llm_provider"), + ) + op.create_index("idx_user_llm_credentials_user", "user_llm_credentials", ["user_id"]) + + +def downgrade() -> None: + op.drop_index("idx_user_llm_credentials_user", table_name="user_llm_credentials") + op.drop_table("user_llm_credentials") diff --git a/db/migrations/versions/0004_research_job_llm_model_id.py b/db/migrations/versions/0004_research_job_llm_model_id.py new file mode 100644 index 0000000..7b85e83 --- /dev/null +++ b/db/migrations/versions/0004_research_job_llm_model_id.py @@ -0,0 +1,32 @@ +"""research_jobs: store BYOK model_id for pipeline + +Revision ID: 0004 +Revises: 0003 +""" +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +revision = "0004" +down_revision = "0003" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "research_jobs", + sa.Column( + "llm_model_id", + sa.String(length=128), + nullable=False, + server_default="grok-3-mini", + ), + ) + op.alter_column("research_jobs", "llm_model_id", server_default=None) + + +def downgrade() -> None: + op.drop_column("research_jobs", "llm_model_id") diff --git a/db/migrations/versions/0005_research_intensity_three_tiers.py b/db/migrations/versions/0005_research_intensity_three_tiers.py new file mode 100644 index 0000000..b45018c --- /dev/null +++ b/db/migrations/versions/0005_research_intensity_three_tiers.py @@ -0,0 +1,42 @@ +"""Map report/job strength to 3 tiers (1=low, 2=med, 3=high) + +Revision ID: 0005 +Revises: 0004 +""" +from __future__ import annotations + +from alembic import op + + +revision = "0005" +down_revision = "0004" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + UPDATE reports SET strength = CASE + WHEN strength <= 3 THEN 1 + WHEN strength <= 7 THEN 2 + ELSE 3 + END + """ + ) + op.execute( + """ + UPDATE research_jobs SET strength = CASE + WHEN strength <= 3 THEN 1 + WHEN strength <= 7 THEN 2 + ELSE 3 + END + """ + ) + op.alter_column("reports", "strength", server_default="2") + op.alter_column("research_jobs", "strength", server_default="2") + + +def downgrade() -> None: + op.alter_column("reports", "strength", server_default="5") + op.alter_column("research_jobs", "strength", server_default="5") diff --git a/db/models.py b/db/models.py new file mode 100644 index 0000000..ece4d8d --- /dev/null +++ b/db/models.py @@ -0,0 +1,335 @@ +""" +SQLAlchemy 2.x async ORM models for Singularity. + +All models inherit from Base. Import this module in db/migrations/env.py +so that Alembic autogenerate can detect schema changes. +""" +from __future__ import annotations + +import uuid +from datetime import datetime +from decimal import Decimal +from typing import Optional + +from sqlalchemy import ( + Boolean, + DateTime, + ForeignKey, + Index, + Integer, + Numeric, + SmallInteger, + String, + Text, + UniqueConstraint, +) +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship +from sqlalchemy.sql import func + + +def _uuid() -> uuid.UUID: + return uuid.uuid4() + + +class _TZDateTime(DateTime): + """DateTime that always stores timezone info (TIMESTAMPTZ in Postgres).""" + def __init__(self): + super().__init__(timezone=True) + + +class Base(DeclarativeBase): + pass + + +# --------------------------------------------------------------------------- +# Users +# --------------------------------------------------------------------------- + + +class User(Base): + __tablename__ = "users" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid) + google_sub: Mapped[str] = mapped_column(String, unique=True, nullable=False) + email: Mapped[str] = mapped_column(String, unique=True, nullable=False) + name: Mapped[Optional[str]] = mapped_column(String, nullable=True) + avatar_url: Mapped[Optional[str]] = mapped_column(String, nullable=True) + created_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False, server_default=func.now()) + last_login_at: Mapped[Optional[datetime]] = mapped_column(_TZDateTime, nullable=True) + daily_token_budget: Mapped[int] = mapped_column(Integer, nullable=False, default=1_000_000) + is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) + + refresh_tokens: Mapped[list[RefreshToken]] = relationship( + back_populates="user", cascade="all, delete-orphan" + ) + reports: Mapped[list[Report]] = relationship( + back_populates="user", cascade="all, delete-orphan" + ) + research_jobs: Mapped[list[ResearchJob]] = relationship( + back_populates="user", cascade="all, delete-orphan" + ) + threads: Mapped[list[Thread]] = relationship( + back_populates="user", cascade="all, delete-orphan" + ) + usage_events: Mapped[list[UsageEvent]] = relationship( + back_populates="user", cascade="all, delete-orphan" + ) + llm_credentials: Mapped[list["UserLlmCredential"]] = relationship( + back_populates="user", cascade="all, delete-orphan" + ) + + __table_args__ = (Index("ix_users_google_sub", "google_sub"),) + + +# --------------------------------------------------------------------------- +# Refresh Tokens +# --------------------------------------------------------------------------- + + +class RefreshToken(Base): + __tablename__ = "refresh_tokens" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid) + user_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + token_hash: Mapped[str] = mapped_column(String, unique=True, nullable=False) + family_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) + created_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False, server_default=func.now()) + expires_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False) + revoked_at: Mapped[Optional[datetime]] = mapped_column(_TZDateTime, nullable=True) + + user: Mapped[User] = relationship(back_populates="refresh_tokens") + + __table_args__ = (Index("idx_rt_user_id", "user_id"),) + + +# --------------------------------------------------------------------------- +# User LLM credentials (BYOK) +# --------------------------------------------------------------------------- + + +class UserLlmCredential(Base): + """ + One encrypted API secret per (user, provider): grok | gemini | deepseek. + """ + + __tablename__ = "user_llm_credentials" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid) + user_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + provider: Mapped[str] = mapped_column(String(32), nullable=False) + encrypted_secret: Mapped[str] = mapped_column(Text, nullable=False) + label: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) + created_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + _TZDateTime, nullable=False, server_default=func.now(), onupdate=func.now() + ) + + user: Mapped[User] = relationship(back_populates="llm_credentials") + + __table_args__ = ( + UniqueConstraint("user_id", "provider", name="uq_user_llm_provider"), + Index("idx_user_llm_credentials_user", "user_id"), + ) + + +# --------------------------------------------------------------------------- +# Reports +# --------------------------------------------------------------------------- + + +class Report(Base): + __tablename__ = "reports" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid) + user_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + title: Mapped[Optional[str]] = mapped_column(String, nullable=True) + query: Mapped[str] = mapped_column(Text, nullable=False) + strength: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=2) + created_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False, server_default=func.now()) + + user: Mapped[User] = relationship(back_populates="reports") + versions: Mapped[list[ReportVersion]] = relationship( + back_populates="report", cascade="all, delete-orphan" + ) + research_jobs: Mapped[list[ResearchJob]] = relationship( + back_populates="report", cascade="all, delete-orphan" + ) + threads: Mapped[list[Thread]] = relationship(back_populates="report") + + __table_args__ = (Index("idx_reports_user_id", "user_id", "created_at"),) + + +# --------------------------------------------------------------------------- +# Report Versions (immutable) +# --------------------------------------------------------------------------- + + +class ReportVersion(Base): + __tablename__ = "report_versions" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid) + report_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("reports.id", ondelete="CASCADE"), nullable=False + ) + version_num: Mapped[int] = mapped_column(Integer, nullable=False) + content_inline: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + content_uri: Mapped[Optional[str]] = mapped_column(String, nullable=True) + content_hash: Mapped[str] = mapped_column(String, nullable=False) + char_count: Mapped[int] = mapped_column(Integer, nullable=False) + patch_instruction: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False, server_default=func.now()) + + report: Mapped[Report] = relationship(back_populates="versions") + + __table_args__ = ( + UniqueConstraint("report_id", "version_num", name="uq_report_version"), + Index("idx_rv_report_id", "report_id", "version_num"), + ) + + +# --------------------------------------------------------------------------- +# Research Jobs +# --------------------------------------------------------------------------- + + +class ResearchJob(Base): + __tablename__ = "research_jobs" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid) + report_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("reports.id", ondelete="CASCADE"), nullable=False + ) + user_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + idempotency_key: Mapped[Optional[str]] = mapped_column(String, nullable=True) + status: Mapped[str] = mapped_column(String, nullable=False, default="pending") + strength: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=2) + # Chat-catalog model_id; full pipeline runs on this model with the user's BYOK key for that provider. + llm_model_id: Mapped[str] = mapped_column(String(128), nullable=False, default="grok-3-mini") + attempts: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0) + max_attempts: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=3) + current_phase: Mapped[Optional[str]] = mapped_column(String, nullable=True) + error_detail: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False, server_default=func.now()) + started_at: Mapped[Optional[datetime]] = mapped_column(_TZDateTime, nullable=True) + finished_at: Mapped[Optional[datetime]] = mapped_column(_TZDateTime, nullable=True) + expires_at: Mapped[Optional[datetime]] = mapped_column(_TZDateTime, nullable=True) + + user: Mapped[User] = relationship(back_populates="research_jobs") + report: Mapped[Report] = relationship(back_populates="research_jobs") + + __table_args__ = ( + Index( + "idx_rj_idempotency", + "user_id", + "idempotency_key", + unique=True, + postgresql_where="idempotency_key IS NOT NULL", + ), + Index("idx_rj_user_status", "user_id", "status"), + ) + + +# --------------------------------------------------------------------------- +# Threads +# --------------------------------------------------------------------------- + + +class Thread(Base): + __tablename__ = "threads" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid) + report_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), ForeignKey("reports.id", ondelete="CASCADE"), nullable=True + ) + user_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + pinned_version_num: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + canonical_report_qa: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=False, server_default="false" + ) + summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + summary_through_message_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), nullable=True + ) + created_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False, server_default=func.now()) + + user: Mapped[User] = relationship(back_populates="threads") + report: Mapped[Optional[Report]] = relationship(back_populates="threads") + messages: Mapped[list[Message]] = relationship( + back_populates="thread", cascade="all, delete-orphan" + ) + + __table_args__ = (Index("idx_threads_user_id", "user_id", "created_at"),) + + +# --------------------------------------------------------------------------- +# Messages +# --------------------------------------------------------------------------- + + +class Message(Base): + __tablename__ = "messages" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid) + thread_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("threads.id", ondelete="CASCADE"), nullable=False + ) + role: Mapped[str] = mapped_column(String, nullable=False) + content: Mapped[str] = mapped_column(Text, nullable=False) + token_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + created_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False, server_default=func.now()) + + thread: Mapped[Thread] = relationship(back_populates="messages") + + __table_args__ = (Index("idx_messages_thread", "thread_id", "created_at"),) + + +# --------------------------------------------------------------------------- +# Usage Events (append-only analytics) +# --------------------------------------------------------------------------- + + +class UsageEvent(Base): + __tablename__ = "usage_events" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid) + user_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + session_id: Mapped[Optional[str]] = mapped_column(String, nullable=True) + event_type: Mapped[str] = mapped_column(String, nullable=False) + model: Mapped[Optional[str]] = mapped_column(String, nullable=True) + prompt_tokens: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + completion_tokens: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + cost_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(10, 6), nullable=True) + route: Mapped[Optional[str]] = mapped_column(String, nullable=True) + report_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True) + job_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True) + thread_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True) + duration_ms: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + success: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True) + error_code: Mapped[Optional[str]] = mapped_column(String, nullable=True) + user_agent: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True) + device_type: Mapped[Optional[str]] = mapped_column(String, nullable=True) + os: Mapped[Optional[str]] = mapped_column(String, nullable=True) + browser: Mapped[Optional[str]] = mapped_column(String, nullable=True) + country: Mapped[Optional[str]] = mapped_column(String, nullable=True) + created_at: Mapped[datetime] = mapped_column(_TZDateTime, nullable=False, server_default=func.now()) + + user: Mapped[User] = relationship(back_populates="usage_events") + + __table_args__ = ( + Index("idx_ue_user_day", "user_id", "created_at"), + Index("idx_ue_event_type", "user_id", "event_type", "created_at"), + ) diff --git a/db/session.py b/db/session.py new file mode 100644 index 0000000..c260c17 --- /dev/null +++ b/db/session.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + +from api.config import settings + +engine = create_async_engine( + settings.database_url, + pool_size=10, + max_overflow=20, + echo=False, + future=True, +) + +AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + async with AsyncSessionLocal() as session: + try: + yield session + except Exception: + await session.rollback() + raise + finally: + await session.close() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dcc1ab5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,80 @@ +version: "3.9" + +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: singularity + POSTGRES_USER: singularity + POSTGRES_PASSWORD: loremipsum + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U singularity"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 + + qdrant: + image: qdrant/qdrant:v1.14.2 + restart: unless-stopped + ports: + - "6333:6333" + volumes: + - qdrant_data:/qdrant/storage + + api: + build: + context: . + dockerfile: Dockerfile.api + restart: unless-stopped + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + qdrant: + condition: service_started + env_file: + - .env + volumes: + - .:/app + command: uvicorn api.main:app --host 0.0.0.0 --port 8000 + + worker: + build: + context: . + dockerfile: Dockerfile.api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + qdrant: + condition: service_started + env_file: + - .env + volumes: + - .:/app + command: python -m workers.main + +volumes: + postgres_data: + qdrant_data: diff --git a/docs/PLATFORM_DEVELOPMENT_GUIDE.md b/docs/PLATFORM_DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..b6d4061 --- /dev/null +++ b/docs/PLATFORM_DEVELOPMENT_GUIDE.md @@ -0,0 +1,901 @@ +# Singularity Platform — Production Development Guide + +> **Last updated:** 2026-04-01 +> **Status:** Active — execution in progress + +--- + +## Table of Contents + +1. [Current State Assessment](#1-current-state-assessment) +2. [Product Specification (Final)](#2-product-specification-final) +3. [System Design Principles](#3-system-design-principles) +4. [Target Architecture](#4-target-architecture) +5. [Agent Team Structure](#5-agent-team-structure) +6. [Backend Development Plan](#6-backend-development-plan) +7. [Frontend Development Plan](#7-frontend-development-plan) +8. [Usage Tracking System](#8-usage-tracking-system) +9. [Edge Cases and Failure Modes](#9-edge-cases-and-failure-modes) +10. [Infrastructure and Hosting](#10-infrastructure-and-hosting) +11. [How to Spawn the Agent Team in Claude Code](#11-how-to-spawn-the-agent-team-in-claude-code) + +--- + +## 1. Current State Assessment + +### Engine (built, do not modify internals) + +| Module | Notes | +|--------|-------| +| `agents/orchestrator/pipeline.py` | Phase-5 pipeline: B→A→C→D | +| `agents/chat/agent.py + thinker.py + executor.py` | Dual-mode chat, streaming-capable | +| `skills/` (44 skills, 3 tiers) | Auto-registered, pluggable | +| `tools/` (14 adapters) | Fallback chains, retries | +| `llm/router.py` | Grok, Gemini, DeepSeek routing | +| `vector_store/client.py` | Qdrant with in-memory fallback | +| `citations/registry.py` | Source provenance | +| `context/budget.py` | Token budget management | +| `models/` | Dataclass contracts | + +### Nothing built yet + +- No web server, no persistence, no auth, no frontend, no job queue, no usage tracking. + +### Two integration entry points + +```python +# 1. Long-running report generation → goes to background worker +from agents.orchestrator.pipeline import run_pipeline +markdown = await run_pipeline(query, strength, ...) + +# 2. Streaming chat (real-time, SSE-suitable) +from agents.chat.agent import ChatAgent +async for chunk in ChatAgent().chat(message, history, active_report_md=...): + yield chunk +``` + +--- + +## 2. Product Specification (Final) + +### 2.1 Authentication +- **Google OAuth only** — no email/password. Landing page = Google sign-in. +- Backend verifies Google ID token, issues own JWT pair. +- User row created on first login (upsert by Google `sub` ID). + +### 2.2 Dashboard (post-login home) +- **Chat bar at the bottom** — same UX as ChatGPT. User types a research query here. +- **Reports grid above** — shows all projects (reports) user has created. Each report card shows title, date, status. +- Typing in the chat bar and submitting → creates a new research job → redirects to the report once done (with live progress shown inline). +- Chat bar also supports direct Q&A (chat mode) without generating a full report. + +### 2.3 Report View (`/reports/[id]`) +- Full rendered markdown report (left/center). +- **Chat panel** (right, slide-in) — follows up with report context + live web access. +- **Text selection → patch flow** — select text in report → "Edit" toolbar appears → instruction modal → LLM patches that section → new version saved. +- Version history accessible via badge. + +### 2.4 Dual-Mode Transitions +- **Chat → Report**: If a chat thread gets substantive, user can click "Generate Report from this conversation" → runs pipeline with chat context as seed → creates a new report. +- **Report → Chat**: Chat panel on the report page is always available, inherits full report as context. + +### 2.5 Design Language +- **Minimalistic base**: Clean white/off-white light mode, or deep neutral dark mode. +- **Animated personality**: Framer Motion — page transitions, chat bubble entrance, streaming text cursor, report card hover lift, gradient shimmer on loading states. +- **No clutter**: No sidebars full of icons. Content-first layout. +- Monospace or geometric sans-serif font (e.g. Inter or Geist). +- Subtle glassmorphism on cards/panels where appropriate. + +### 2.6 Usage Tracking (Backend) +Track every meaningful event for analytics, billing, and debugging: +- What model, how many tokens, cost in USD +- What feature (research_job, chat_qa, patch, etc.) +- What time, device type, OS, browser, country (from IP) +- Success/failure, duration, linked report/job + +--- + +## 3. System Design Principles + +| # | Principle | +|---|-----------| +| P1 | **Engine purity** — agents/ has zero HTTP/DB imports. Platform wraps the engine. | +| P2 | **Async-first** — no route handler blocks. Long work = background worker. | +| P3 | **Ownership on every row** — every table has `user_id`. Service layer enforces it. | +| P4 | **Idempotency** — duplicate research jobs within 24h return the same job. | +| P5 | **Optimistic locking** — patches use ETag + `If-Match`. 409 on conflict. | +| P6 | **Costs are first-class** — quota checked before expensive operations. | +| P7 | **Fail loudly at boundaries** — structured JSON errors, no raw tracebacks. | +| P8 | **Usage tracking is non-blocking** — emitted async, never delays the request. | + +--- + +## 4. Target Architecture + +``` +┌────────────────────────────────────────────────────────────────┐ +│ Next.js 14 (Vercel) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Landing (Google Sign-in) → Dashboard (Chat + Reports) │ │ +│ │ Report View (Markdown + Chat Panel + Patch toolbar) │ │ +│ └─────────────────────────────┬───────────────────────────┘ │ +└────────────────────────────────┼───────────────────────────────┘ + │ REST + SSE +┌────────────────────────────────▼───────────────────────────────┐ +│ FastAPI /api/v1 │ +│ /auth/google /research/jobs /reports /threads │ +│ /users/me/stats /users/me/usage │ +├────────────────────────────────────────────────────────────────┤ +│ Middleware: JWT auth · Rate limit · Request ID · Usage emitter│ +└──────────────┬───────────────────────────────┬─────────────────┘ + │ enqueue │ read/write +┌──────────────▼──────────────┐ ┌────────────▼────────────────┐ +│ ARQ Workers (Redis) │ │ Data Layer │ +│ run_research_job() │ │ PostgreSQL (SQLAlchemy 2) │ +│ run_patch_job() │ │ Redis (queue + cache + SSE)│ +│ run_summary_job() │ │ S3/R2 (markdown blobs) │ +└─────────────────────────────┘ │ Qdrant (vector search) │ + └─────────────────────────────┘ +``` + +--- + +## 5. Agent Team Structure + +### Roles and Responsibilities + +``` +┌─────────────────────────────────────────────────────────────┐ +│ TECH LEAD (Supervisor) │ +│ Reviews all PRs. Resolves conflicts. Final arch decisions. │ +│ Unblocks agents. Writes integration tests. │ +└────────┬──────────┬───────────────┬──────────┬──────────────┘ + │ │ │ │ + ┌────▼───┐ ┌────▼────┐ ┌──────▼───┐ ┌────▼─────┐ + │ BE-1 │ │ BE-2 │ │ BE-3 │ │ FE-1 │ + │ Infra │ │ Auth + │ │ Reports │ │ Auth + │ + │ Docker │ │ Jobs + │ │ Patch + │ │ Dashboard│ + │ CI/CD │ │ Workers │ │ Usage │ │ Landing │ + └────────┘ └─────────┘ └──────────┘ └──────────┘ + │ + ┌────▼─────┐ + │ FE-2 │ + │ Report │ + │ View + │ + │ Chat UI │ + └──────────┘ + │ + ┌────▼─────────────────────────────────────────┐ + │ QA / TESTER │ + │ Writes pytest + Playwright E2E. Reviews │ + │ all new endpoints for missing coverage. │ + └──────────────────────────────────────────────┘ +``` + +### Task Assignment + +| Agent | Owns | Deliverables | +|-------|------|-------------| +| **BE-1 (Infra)** | `docker-compose.yml`, `Dockerfile`, `Makefile`, `requirements_api.txt`, `pyproject.toml`, `.env.example`, CI pipeline | Running dev environment | +| **BE-2 (Core API)** | `api/`, `db/`, `api/auth/`, `api/research/`, `workers/` | Auth + research jobs + SSE | +| **BE-3 (Features)** | `api/reports/`, `api/threads/`, `patch/`, `api/users/`, usage tracking middleware | Reports, Q&A, patch, stats | +| **FE-1 (Foundation)** | `frontend/` setup, Google Auth, landing page, dashboard shell, design system | Authenticated app shell | +| **FE-2 (Report UI)** | Report viewer, chat panel, streaming, patch flow, version history | Core UX flows | +| **Tech Lead** | Integration, `models/api_schemas.py` bridge, architecture decisions | Cohesion, quality | +| **QA** | `tests/api/`, `tests/workers/`, `frontend/e2e/` | >80% coverage, E2E critical path | + +--- + +## 6. Backend Development Plan + +### 6.1 Project Structure + +``` +singularity/ +├── api/ +│ ├── main.py # App factory, lifespan, middleware stack +│ ├── config.py # pydantic-settings, all env vars +│ ├── deps.py # get_db, get_current_user, get_redis +│ ├── auth/ +│ │ ├── router.py # POST /auth/google, POST /auth/refresh, POST /auth/logout +│ │ ├── service.py # verify Google ID token, JWT issue, user upsert +│ │ └── schemas.py # GoogleAuthRequest, TokenPair +│ ├── research/ +│ │ ├── router.py # POST /jobs, GET /jobs/{id}, GET /jobs/{id}/events, POST /jobs/{id}/cancel +│ │ ├── service.py # Job lifecycle, idempotency, SSE pub +│ │ └── schemas.py +│ ├── reports/ +│ │ ├── router.py # CRUD, versions, export +│ │ ├── service.py # Ownership check, blob load +│ │ └── schemas.py +│ ├── threads/ +│ │ ├── router.py # POST /threads, POST /threads/{id}/messages (SSE) +│ │ ├── service.py # Context assembly, message persist, summary +│ │ └── schemas.py +│ ├── users/ +│ │ ├── router.py # GET /me, GET /me/stats, GET /me/usage +│ │ ├── service.py # Aggregate usage events, compute stats +│ │ └── schemas.py # UsageStats, UsageEvent, DeviceBreakdown +│ └── middleware/ +│ ├── auth.py +│ ├── rate_limit.py # Redis sliding window +│ ├── request_id.py +│ └── usage_emitter.py # Async emit after response +├── db/ +│ ├── models.py # All ORM models +│ ├── session.py # Async engine, session factory +│ └── migrations/ # Alembic +│ ├── env.py +│ └── versions/ +├── workers/ +│ ├── main.py # ARQ WorkerSettings +│ ├── research_job.py # run_research_job task +│ ├── patch_job.py # run_patch_job (async patch via LLM) +│ └── summary_job.py # thread rolling summary +├── storage/ +│ ├── base.py # BlobStore protocol +│ ├── s3.py # S3 / R2 +│ └── local.py # dev +└── patch/ + ├── service.py + ├── slug.py + └── validator.py +``` + +### 6.2 Database Schema + +```sql +-- Users +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + google_sub TEXT UNIQUE NOT NULL, -- Google OAuth sub + email TEXT UNIQUE NOT NULL, + name TEXT, + avatar_url TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_login_at TIMESTAMPTZ, + daily_token_budget INTEGER NOT NULL DEFAULT 1000000, + is_active BOOLEAN NOT NULL DEFAULT true +); + +-- Refresh Tokens +CREATE TABLE refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash TEXT UNIQUE NOT NULL, -- SHA-256 of the raw token + family_id UUID NOT NULL, -- rotation family (reuse = revoke all) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ +); +CREATE INDEX idx_rt_user_id ON refresh_tokens(user_id); + +-- Reports +CREATE TABLE reports ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title TEXT, + query TEXT NOT NULL, + strength SMALLINT NOT NULL DEFAULT 5, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX idx_reports_user_id ON reports(user_id, created_at DESC); + +-- Report Versions (immutable) +CREATE TABLE report_versions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + report_id UUID NOT NULL REFERENCES reports(id) ON DELETE CASCADE, + version_num INTEGER NOT NULL, + content_inline TEXT, -- NULL if blob + content_uri TEXT, -- S3 key if large (>500KB) + content_hash TEXT NOT NULL, -- SHA-256, used as ETag + char_count INTEGER NOT NULL, + patch_instruction TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (report_id, version_num) +); + +-- Research Jobs +CREATE TABLE research_jobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + report_id UUID NOT NULL REFERENCES reports(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + idempotency_key TEXT, + status TEXT NOT NULL DEFAULT 'pending', -- pending|running|done|failed|cancelled + strength SMALLINT NOT NULL DEFAULT 5, + attempts SMALLINT NOT NULL DEFAULT 0, + max_attempts SMALLINT NOT NULL DEFAULT 3, + current_phase TEXT, -- B|A|C|D for progress display + error_detail TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + started_at TIMESTAMPTZ, + finished_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ +); +CREATE UNIQUE INDEX idx_rj_idempotency ON research_jobs(user_id, idempotency_key) + WHERE idempotency_key IS NOT NULL; + +-- Threads +CREATE TABLE threads ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + report_id UUID REFERENCES reports(id) ON DELETE CASCADE, -- NULL = pure chat + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + pinned_version_num INTEGER, + summary TEXT, + summary_through_message_id UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Messages +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + thread_id UUID NOT NULL REFERENCES threads(id) ON DELETE CASCADE, + role TEXT NOT NULL, -- 'user' | 'assistant' + content TEXT NOT NULL, + token_count INTEGER, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX idx_messages_thread ON messages(thread_id, created_at); + +-- Usage Events (append-only, comprehensive) +CREATE TABLE usage_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + session_id TEXT, + event_type TEXT NOT NULL, -- 'llm_call'|'report_generate'|'chat_message'|'patch'|'report_view' + model TEXT, + prompt_tokens INTEGER, + completion_tokens INTEGER, + cost_usd NUMERIC(10,6), + route TEXT, -- 'research_job'|'chat_qa'|'patch'|'thread_summary' + report_id UUID, + job_id UUID, + thread_id UUID, + duration_ms INTEGER, + success BOOLEAN, + error_code TEXT, + -- Client context + user_agent TEXT, + ip_address INET, + device_type TEXT, -- 'desktop'|'mobile'|'tablet' + os TEXT, -- 'macOS'|'Windows'|'iOS'|'Android'|'Linux' + browser TEXT, -- 'Chrome'|'Safari'|'Firefox'|'Edge' + country TEXT, -- ISO-3166 from IP + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX idx_ue_user_day ON usage_events(user_id, created_at DESC); +CREATE INDEX idx_ue_event_type ON usage_events(user_id, event_type, created_at DESC); +``` + +### 6.3 API Contract + +``` +Auth: + POST /api/v1/auth/google { id_token } → TokenPair + POST /api/v1/auth/refresh { refresh_token } → TokenPair + POST /api/v1/auth/logout (revokes refresh token) + +Users / Stats: + GET /api/v1/users/me → UserProfile + GET /api/v1/users/me/stats → { total_reports, total_tokens, cost_usd, streak_days, ... } + GET /api/v1/users/me/usage ?range=7d|30d|90d → time-series for graphs + GET /api/v1/users/me/usage/models → model breakdown pie data + GET /api/v1/users/me/usage/devices → device/OS/browser breakdown + +Research Jobs: + POST /api/v1/research/jobs { query, strength?, idempotency_key? } → JobCreated + GET /api/v1/research/jobs/{id} → JobStatus + GET /api/v1/research/jobs/{id}/events SSE → job_status|job_done|job_error + POST /api/v1/research/jobs/{id}/cancel → 202 + +Reports: + GET /api/v1/reports cursor-paginated → ReportList + GET /api/v1/reports/{id} → ReportMeta + GET /api/v1/reports/{id}/versions → VersionList + GET /api/v1/reports/{id}/versions/{v} → { content, etag } + POST /api/v1/reports/{id}/versions/{v}/patch If-Match → 201|409|422 + GET /api/v1/reports/{id}/versions/{v}/export ?format=md|html → file + +Threads: + POST /api/v1/threads { report_id?, pinned_version? } → ThreadCreated + GET /api/v1/threads/{id} → ThreadWithMessages + POST /api/v1/threads/{id}/messages SSE → plan|step_start|token|step_end|done|error +``` + +### 6.4 Google OAuth Flow + +``` +Frontend (NextAuth.js) Backend (FastAPI) +───────────────────── ───────────────── +User clicks "Sign in with Google" + → NextAuth redirects to Google + → User consents + → Google returns ID token to NextAuth + → NextAuth stores session + → Frontend calls POST /api/v1/auth/google + with { id_token: "..." } + Verifies token with Google (google-auth-library) + Extracts: sub, email, name, picture + UPSERT users ON CONFLICT(google_sub) + Issues JWT access (15min) + refresh (30d) + Returns TokenPair + → Frontend stores tokens (httpOnly cookie or memory) + → Redirects to /dashboard +``` + +### 6.5 Worker Architecture + +```python +# workers/research_job.py +async def run_research_job(ctx, job_id: str) -> None: + job = await db.get_job(job_id) + if job.status in ("done", "cancelled"): return # idempotent + + cancel_token = CancelToken(lambda: datetime.utcnow() > job.expires_at) + + async def phase_progress(phase: str, desc: str): + await db.update_job_phase(job_id, phase) + await redis_publish(job_id, "job_status", {"phase": phase, "description": desc}) + + try: + markdown = await run_pipeline( + query=job.query, strength=job.strength, + cancel_token=cancel_token, + on_phase=phase_progress, + ) + version = await create_report_version(db, storage, job.report_id, markdown) + await db.update_job(job_id, status="done", finished_at=now()) + await redis_publish(job_id, "job_done", {"version_num": version.version_num}) + except Cancelled: + await db.update_job(job_id, status="cancelled") + except Exception as e: + if job.attempts >= job.max_attempts: + await db.update_job(job_id, status="failed", error=str(e)[:500]) + await redis_publish(job_id, "job_error", {"message": str(e)}) + else: + raise # ARQ retries with backoff +``` + +--- + +## 7. Frontend Development Plan + +### 7.1 Stack + +``` +Next.js 14 (App Router) + TypeScript strict +Tailwind CSS + shadcn/ui +Framer Motion (animations) +NextAuth.js (Google OAuth) +Tanstack Query v5 (data fetching + cache) +Zustand (client state: report, selection, chat) +react-markdown + remark-math + rehype-katex (report rendering) +OpenAPI codegen client (from FastAPI /openapi.json) +Playwright (E2E tests) +``` + +### 7.2 Routes + +``` +/ Landing — Google sign-in + product tagline +/dashboard Post-login home — Chat bar (bottom) + Reports grid (top) +/reports/[id] Report view — Markdown left, Chat panel right +/reports/[id]/history Version history +/profile Usage stats dashboard (graphs, model breakdown, device) +``` + +### 7.3 Landing Page Design Spec + +- Full-viewport dark gradient (deep navy → near-black) +- Centered: product name, one-line tagline, large "Sign in with Google" button +- Background: subtle animated particle mesh or flowing gradient blobs (CSS/canvas) +- No nav, no clutter. One action. + +### 7.4 Dashboard Design Spec + +``` +┌─────────────────────────────────────────────────────────┐ +│ Singularity [avatar] [⚙] │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ Recent Projects [+ New] │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Report title │ │ Report title │ │ Report title │ │ +│ │ 2 days ago │ │ 1 week ago │ │ 3 weeks ago │ │ +│ │ 45k chars │ │ 32k chars │ │ 12k chars │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ ───────────────────────────────────────────────────── │ +│ │ Ask anything or describe a research topic... [→] │ +│ ───────────────────────────────────────────────────── │ +└─────────────────────────────────────────────────────────┘ +``` + +- Chat bar stays fixed at the bottom. On submit: if "Generate report" mode → creates research job with live progress overlay. If "chat" mode → opens a thread inline above the bar. +- Report cards: hover lifts with shadow, click → `/reports/[id]` +- Strength selector: inline popover before submit (1–10 slider, cost estimate) + +### 7.5 Report View Design Spec + +``` +┌──────────────────────────────────┬────────────────────┐ +│ ← Dashboard Report Title v3 │ Q&A with Report │ +├──────────────────────────────────┤ │ +│ │ [message bubbles] │ +│ # Report Title │ │ +│ ## Section 1 │ │ +│ Lorem ipsum... │ │ +│ [selected text → toolbar] │ ───────────────── │ +│ │ Ask about this... │ +│ ## Section 2 └────────────────────┘ +│ ... +└────────────────────────────────── +``` + +- Chat panel slides in from the right (Framer Motion). +- Patch toolbar floats above selection. +- Report renders with syntax highlighting, math (KaTeX), tables. + +### 7.6 Key Components + +``` +components/ +├── auth/ +│ └── GoogleSignInButton.tsx +├── dashboard/ +│ ├── ReportCard.tsx # Hover animation, status badge +│ ├── ReportGrid.tsx # Masonry or uniform grid +│ ├── ChatBar.tsx # Bottom bar, mode toggle, strength picker +│ └── JobProgressOverlay.tsx # SSE consumer, phase display, animated loader +├── report/ +│ ├── ReportViewer.tsx # react-markdown, selection handler +│ ├── SelectionToolbar.tsx # Floating edit/copy actions +│ ├── PatchModal.tsx # Instruction input, diff preview +│ ├── VersionBadge.tsx # vN badge + history link +│ └── ChatToReportButton.tsx # "Generate report from conversation" +├── chat/ +│ ├── ChatPanel.tsx # Slide-in panel +│ ├── MessageBubble.tsx # User / assistant +│ ├── StreamingMessage.tsx # Token-by-token SSE render +│ ├── PlanDisplay.tsx # Step-by-step plan display +│ └── SourceList.tsx # Grounding citations +├── profile/ +│ ├── UsageChart.tsx # Recharts time-series (tokens/cost) +│ ├── ModelBreakdown.tsx # Pie chart by model +│ └── DeviceBreakdown.tsx # OS/browser bar chart +└── shared/ + ├── SSEConsumer.tsx # useSSE hook wrapper + ├── ErrorBoundary.tsx + └── AnimatedPage.tsx # Framer Motion page transition wrapper +``` + +### 7.7 Animations Spec (Framer Motion) + +| Interaction | Animation | +|-------------|-----------| +| Page transition | `opacity: 0→1, y: 8→0, duration: 0.25s` | +| Report card hover | `scale: 1→1.02, shadow lift, duration: 0.15s` | +| Chat panel open | `x: 100%→0, duration: 0.3s, ease: easeOut` | +| Message bubble enter | `opacity: 0→1, y: 6→0, stagger 0.05s` | +| Streaming cursor | Blinking `|` at text end while streaming | +| Job progress phases | Phase label fades in/out, progress bar animates | +| Patch modal | Scale in from center `scale: 0.95→1` | +| Loading skeleton | Shimmer gradient sweep `@keyframes shimmer` | + +--- + +## 8. Usage Tracking System + +### 8.1 Tracking Strategy + +Every meaningful server-side event emits a `UsageEvent`. This is **always async and non-blocking** — it runs after the response is sent. + +```python +# api/middleware/usage_emitter.py + +class UsageEmitterMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + response = await call_next(request) + # Fire-and-forget: never delays response + asyncio.create_task( + emit_usage_event(request, response) + ) + return response + +async def emit_usage_event(request: Request, response: Response): + user_id = getattr(request.state, "user_id", None) + if not user_id: return + + # Parse User-Agent + ua = parse_user_agent(request.headers.get("user-agent", "")) + + # Geo from IP (cached in Redis, 1h TTL) + country = await get_country_from_ip(request.client.host) + + event = UsageEvent( + user_id=user_id, + session_id=request.headers.get("X-Session-ID"), + event_type=classify_route(request.url.path), + duration_ms=request.state.duration_ms, + success=response.status_code < 400, + user_agent=request.headers.get("user-agent"), + ip_address=request.client.host, + device_type=ua.device_type, + os=ua.os, + browser=ua.browser, + country=country, + ) + await db.insert_usage_event(event) +``` + +### 8.2 LLM Cost Tracking + +Each LLM call in the engine emits a structured cost event. We hook into this via a lightweight observer: + +```python +# Injected into ExecutionContext before pipeline starts +context.on_llm_call = lambda event: task_queue.put_nowait(event) + +# Worker drains queue after pipeline completes +for event in task_queue: + await db.insert_usage_event(UsageEvent( + user_id=job.user_id, job_id=job.id, + event_type="llm_call", + model=event.model, + prompt_tokens=event.prompt_tokens, + completion_tokens=event.completion_tokens, + cost_usd=calculate_cost(event.model, event.prompt_tokens, event.completion_tokens), + route="research_job", + )) +``` + +### 8.3 Stats API Response Shape + +```json +GET /api/v1/users/me/stats +{ + "total_reports": 47, + "total_tokens": 4820000, + "total_cost_usd": 12.45, + "reports_this_week": 5, + "tokens_today": 45000, + "tokens_remaining_today": 955000, + "streak_days": 7, + "avg_report_strength": 6.2, + "favorite_model": "grok-3", + "most_active_hour": 14 +} + +GET /api/v1/users/me/usage?range=30d +{ + "series": [ + { "date": "2026-03-01", "tokens": 45000, "cost_usd": 0.12, "reports": 2 }, + ... + ], + "total_tokens": 1200000, + "total_cost_usd": 3.20 +} + +GET /api/v1/users/me/usage/models +{ + "breakdown": [ + { "model": "grok-3", "tokens": 800000, "cost_usd": 2.10, "pct": 66 }, + { "model": "gemini-2.0-flash", "tokens": 400000, "cost_usd": 1.10, "pct": 34 } + ] +} +``` + +--- + +## 9. Edge Cases and Failure Modes + +### Research Jobs +| Scenario | Handling | +|----------|---------| +| Worker crashes mid-pipeline | Heartbeat miss → DLQ after 10 min. Next attempt picks up (idempotent, retries up to max_attempts). | +| User cancels mid-run | `POST /cancel` sets `expires_at=now()`. Engine checks cancel_token at phase boundaries. | +| Same query submitted twice | Idempotency key returns existing job (200, not 201) within 24h. | +| All LLM providers fail | After 3 retries across providers → job `failed`, error: `llm_unavailable`. | +| Job times out | ARQ `job_timeout=1800s`. Sets `failed`, `error: timeout`. | + +### Patch +| Scenario | Handling | +|----------|---------| +| Selection not found verbatim | Fuzzy match (difflib >0.85). Still fails → 422. | +| LLM generates oversized output | Reject if new section >3× original. 422. | +| Concurrent patch by same user | ETag SELECT FOR UPDATE. Second writer → 409. | +| LLM injects HTML/script in MD | MD AST validation strips script tags. Reject if unsafe nodes present. | + +### SSE +| Scenario | Handling | +|----------|---------| +| Client disconnects | Worker continues async. Client reconnects with `Last-Event-ID`, replays from cursor. | +| Redis pub/sub fails | SSE falls back to DB polling at 3s interval. | +| Proxy strips chunked encoding | `X-Accel-Buffering: no` header on all SSE responses. | +| SSE auth (EventSource can't send headers) | Short-lived SSE token (30s, signed) passed as query param. Generated by `GET /auth/sse-token`. | + +### Auth +| Scenario | Handling | +|----------|---------| +| Google token expired/invalid | 401 with `code: invalid_google_token`. | +| Refresh token reuse after rotation | Revoke entire family. User must re-authenticate. | +| Account deactivated | `is_active=false` → 401. | + +--- + +## 10. Infrastructure and Hosting + +### Services + +| Service | Dev | Production | +|---------|-----|------------| +| FastAPI | `uvicorn --reload` | Docker on Fly.io or Railway | +| ARQ Workers | Same container (1 worker) | Separate container (2–4 workers) | +| PostgreSQL | Docker Compose | Supabase or Neon (managed) | +| Redis | Docker Compose | Upstash Redis | +| Blob store | Local filesystem | Cloudflare R2 | +| Next.js | `next dev` | Vercel | +| Qdrant | In-memory | Qdrant Cloud | + +### New Dependencies + +``` +# requirements_api.txt (additions to existing requirements.txt) +fastapi>=0.111 +uvicorn[standard]>=0.29 +sqlalchemy[asyncio]>=2.0 +alembic>=1.13 +asyncpg>=0.29 +pydantic-settings>=2.2 +arq>=0.25 +redis>=5.0 +httpx>=0.27 +google-auth>=2.29 # Google ID token verification +authlib>=1.3 # OAuth2 flows +python-jose[cryptography] # JWT +bcrypt>=4.1 +python-multipart>=0.0.9 +aioboto3>=12.3 # S3/R2 async +user-agents>=2.2.0 # UA parsing +``` + +### Docker Compose + +```yaml +version: "3.9" +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: singularity + POSTGRES_USER: singularity + POSTGRES_PASSWORD: dev_password + ports: ["5432:5432"] + volumes: [postgres_data:/var/lib/postgresql/data] + + redis: + image: redis:7-alpine + ports: ["6379:6379"] + + api: + build: { context: ., dockerfile: Dockerfile.api } + command: uvicorn api.main:app --reload --host 0.0.0.0 --port 8000 + ports: ["8000:8000"] + depends_on: [postgres, redis] + env_file: .env + volumes: [.:/app] + + worker: + build: { context: ., dockerfile: Dockerfile.api } + command: python -m workers.main + depends_on: [postgres, redis] + env_file: .env + volumes: [.:/app] + +volumes: + postgres_data: +``` + +--- + +## 11. How to Spawn the Agent Team in Claude Code + +Claude Code supports multi-agent execution through **git worktrees** and **multiple terminal sessions**. Each agent runs as a separate `claude` process with an isolated copy of the repo. + +### Method 1: Git Worktrees (Recommended — True Isolation) + +```bash +# From the main repo dir +# Create a worktree + branch for each agent +git worktree add ../sing-infra -b feat/infra +git worktree add ../sing-be-core -b feat/be-core +git worktree add ../sing-be-feat -b feat/be-features +git worktree add ../sing-fe-found -b feat/fe-foundation +git worktree add ../sing-fe-ui -b feat/fe-report-ui + +# Then in each worktree, open Claude Code +# iTerm2: ⌘T for new tab, or use tmux +cd ../sing-infra && claude +cd ../sing-be-core && claude +cd ../sing-be-feat && claude +cd ../sing-fe-found && claude +cd ../sing-fe-ui && claude +``` + +Each `claude` session has its own repo copy. They commit to their branch. Tech Lead merges. + +### Method 2: tmux Multi-Pane (Fastest Setup) + +```bash +# Install tmux if needed: brew install tmux +tmux new-session -s singularity + +# Split into panes +# Ctrl+B, then % (vertical split) +# Ctrl+B, then " (horizontal split) +# Ctrl+B, then o (switch pane) + +# Pane 1: BE-1 Infra +cd /Users/nishant/Desktop/singularity && claude + +# Pane 2: BE-2 Core API (after git worktree add) +cd ../sing-be-core && claude + +# etc. +``` + +### Method 3: Non-Interactive Mode (Fully Automated) + +```bash +# Run claude with a task description, no human in the loop +# Each runs in background +claude --dangerously-skip-permissions \ + -p "$(cat docs/agent_prompts/be_infra.md)" \ + --output-format stream-json \ + > logs/be_infra.log 2>&1 & + +claude --dangerously-skip-permissions \ + -p "$(cat docs/agent_prompts/be_core.md)" \ + --output-format stream-json \ + > logs/be_core.log 2>&1 & + +claude --dangerously-skip-permissions \ + -p "$(cat docs/agent_prompts/fe_found.md)" \ + --output-format stream-json \ + > logs/fe_found.log 2>&1 & +``` + +### Recommended Workflow + +``` +1. Tech Lead (this session) writes shared contracts first: + - db/models.py (so BE agents agree on schema) + - models/api_schemas.py (Pydantic bridge layer) + - api/config.py (all env vars agreed) + +2. Spawn BE-1 (Infra) immediately — no dependencies. + +3. Spawn BE-2 (Core API) after contracts are written. + +4. Spawn FE-1 (Foundation) immediately — frontend has no backend dep yet. + +5. Spawn BE-3 (Features) after BE-2 finishes auth + DB. + +6. Spawn FE-2 (Report UI) after FE-1 shell + BE-2 APIs are ready. + +7. QA runs last — writes tests against the complete API. +``` + +### Agent Prompt Template + +When spawning each agent, pass a prompt like: + +``` +You are [ROLE] on the Singularity platform team. +Read docs/PLATFORM_DEVELOPMENT_GUIDE.md for the full spec. +Your specific task: [TASK DESCRIPTION] +Files you own: [FILE LIST] +Files to READ but NOT modify: [FILE LIST] +Definition of done: [CHECKLIST] +Write production-quality code. No TODOs, no stubs. Commit when done. +``` diff --git a/favicon_io/android-chrome-192x192.png b/favicon_io/android-chrome-192x192.png new file mode 100644 index 0000000..8e806a7 Binary files /dev/null and b/favicon_io/android-chrome-192x192.png differ diff --git a/favicon_io/android-chrome-512x512.png b/favicon_io/android-chrome-512x512.png new file mode 100644 index 0000000..19e879c Binary files /dev/null and b/favicon_io/android-chrome-512x512.png differ diff --git a/favicon_io/apple-touch-icon.png b/favicon_io/apple-touch-icon.png new file mode 100644 index 0000000..9a76b1e Binary files /dev/null and b/favicon_io/apple-touch-icon.png differ diff --git a/favicon_io/favicon-16x16.png b/favicon_io/favicon-16x16.png new file mode 100644 index 0000000..ef3a68b Binary files /dev/null and b/favicon_io/favicon-16x16.png differ diff --git a/favicon_io/favicon-32x32.png b/favicon_io/favicon-32x32.png new file mode 100644 index 0000000..f01b40b Binary files /dev/null and b/favicon_io/favicon-32x32.png differ diff --git a/favicon_io/favicon.ico b/favicon_io/favicon.ico new file mode 100644 index 0000000..82b7338 Binary files /dev/null and b/favicon_io/favicon.ico differ diff --git a/favicon_io/site.webmanifest b/favicon_io/site.webmanifest new file mode 100644 index 0000000..887dd35 --- /dev/null +++ b/favicon_io/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Singularity", + "short_name": "Singularity", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#f7f5f0", + "background_color": "#f7f5f0", + "display": "standalone" +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 0000000..8bd0e39 --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..66e1566 --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ + reactCompiler: true, +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..c8de338 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,10421 @@ +{ + "name": "frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.1.0", + "dependencies": { + "@auth/core": "^0.41.0", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.96.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.38.0", + "katex": "^0.16.44", + "lucide-react": "^1.7.0", + "next": "16.2.2", + "next-auth": "^5.0.0-beta.30", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-markdown": "^10.1.0", + "recharts": "^3.8.1", + "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "babel-plugin-react-compiler": "1.0.0", + "eslint": "^9", + "eslint-config-next": "16.2.2", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@auth/core": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz", + "integrity": "sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@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.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", + "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.2.tgz", + "integrity": "sha512-IOPbWzDQ+76AtjZioaCjpIY72xNSDMnarZ2GMQ4wjNLvnJEJHqxQwGFhgnIWLV9klb4g/+amg88Tk5OXVpyLTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz", + "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz", + "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz", + "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz", + "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz", + "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz", + "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz", + "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz", + "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.96.1.tgz", + "integrity": "sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.96.1.tgz", + "integrity": "sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.96.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz", + "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", + "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001784", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", + "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.330", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.330.tgz", + "integrity": "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@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.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "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.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.2.tgz", + "integrity": "sha512-6VlvEhwoug2JpVgjZDhyXrJXUEuPY++TddzIpTaIRvlvlXXFgvQUtm3+Zr84IjFm0lXtJt73w19JA08tOaZVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.2.2", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "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" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/katex": { + "version": "0.16.44", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.44.tgz", + "integrity": "sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", + "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.2", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.2", + "@next/swc-darwin-x64": "16.2.2", + "@next/swc-linux-arm64-gnu": "16.2.2", + "@next/swc-linux-arm64-musl": "16.2.2", + "@next/swc-linux-x64-gnu": "16.2.2", + "@next/swc-linux-x64-musl": "16.2.2", + "@next/swc-win32-arm64-msvc": "16.2.2", + "@next/swc-win32-x64-msvc": "16.2.2", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "5.0.0-beta.30", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.30.tgz", + "integrity": "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/oauth4webapi": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz", + "integrity": "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "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 + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "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 + } + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "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 + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "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 + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..afda345 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@auth/core": "^0.41.0", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.96.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.38.0", + "katex": "^0.16.44", + "lucide-react": "^1.7.0", + "next": "16.2.2", + "next-auth": "^5.0.0-beta.30", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-markdown": "^10.1.0", + "recharts": "^3.8.1", + "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "babel-plugin-react-compiler": "1.0.0", + "eslint": "^9", + "eslint-config-next": "16.2.2", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/frontend/public/android-chrome-192x192.png b/frontend/public/android-chrome-192x192.png new file mode 100644 index 0000000..8e806a7 Binary files /dev/null and b/frontend/public/android-chrome-192x192.png differ diff --git a/frontend/public/android-chrome-512x512.png b/frontend/public/android-chrome-512x512.png new file mode 100644 index 0000000..19e879c Binary files /dev/null and b/frontend/public/android-chrome-512x512.png differ diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..9a76b1e Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/favicon-16x16.png b/frontend/public/favicon-16x16.png new file mode 100644 index 0000000..ef3a68b Binary files /dev/null and b/frontend/public/favicon-16x16.png differ diff --git a/frontend/public/favicon-32x32.png b/frontend/public/favicon-32x32.png new file mode 100644 index 0000000..f01b40b Binary files /dev/null and b/frontend/public/favicon-32x32.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..82b7338 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/site.webmanifest b/frontend/public/site.webmanifest new file mode 100644 index 0000000..887dd35 --- /dev/null +++ b/frontend/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Singularity", + "short_name": "Singularity", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#f7f5f0", + "background_color": "#f7f5f0", + "display": "standalone" +} diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/api/auth/[...nextauth]/route.ts b/frontend/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..86c9f3d --- /dev/null +++ b/frontend/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx new file mode 100644 index 0000000..ce44743 --- /dev/null +++ b/frontend/src/app/dashboard/page.tsx @@ -0,0 +1,748 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { LayoutGrid, Trash2 } from "lucide-react"; +import { + reportsApi, + jobsApi, + threadsApi, + llmApi, + DEFAULT_CHAT_MODEL_ID, + type ReportMeta, + type ThreadSummaryResponse, +} from "@/lib/api"; +import { llmModelGroupsFromCatalog } from "@/lib/llm_model_groups"; +import { ChatModelPicker } from "@/components/chat/ChatModelPicker"; +import { showDebugMockResearchControls } from "@/lib/debug_research_mock"; +import { cn } from "@/lib/cn"; +import { + formatRelative, + truncateDisplayLabel, + researchIntensityLabel, + RESEARCH_INTENSITY_OPTIONS, +} from "@/lib/utils"; +import { UserMenu } from "@/components/user-menu"; +import { DeleteReportDialog } from "@/components/delete-report-dialog"; +import { DeleteThreadDialog } from "@/components/delete_thread_dialog"; +import { + ChatPanel, + type ChatPanelLaunchPayload, +} from "@/components/chat/ChatPanel"; +import { ChatHistorySidebar } from "@/components/chat/chat_history_sidebar"; +import { AppLogoMark } from "@/components/app-logo"; +import { AccountReconnectPrompt } from "@/components/account_reconnect_prompt"; + +/** Matches `var(--rpt-bg)` — editorial paper, same family as the research report page */ +const DASH_MAIN_BG_RGB = "247,245,240"; + +const chromePillStyle = { + background: `radial-gradient(ellipse 145% 195% at 50% 22%, rgba(${DASH_MAIN_BG_RGB},0.99) 0%, rgba(${DASH_MAIN_BG_RGB},0.95) 38%, rgba(${DASH_MAIN_BG_RGB},0.82) 58%, rgba(${DASH_MAIN_BG_RGB},0.42) 80%, rgba(${DASH_MAIN_BG_RGB},0.08) 94%, rgba(${DASH_MAIN_BG_RGB},0) 100%)`, +} as const; + +/** + * Purpose: Replace the full-width header with centered brand + top-right controls. + * Inputs: `onBackToProjects` when a thread is open (returns to the projects grid). + * Outputs: Absolutely positioned layers; pointer-events pass through except interactive chips. + * ConciseExplanation: Full-width short linear scrim matches main paper background with a dense opaque + * top band; Projects sits beside the account menu as a matching minimal icon button. + */ +function DashboardMainFloatingChrome({ + onBackToProjects, +}: { + onBackToProjects?: () => void; +}) { + return ( + <> +
+
+
+
+ + + Singularity + +
+
+
+
+ {onBackToProjects ? ( +
+ +
+ ) : null} +
+ +
+
+ + ); +} + +function ReportCard({ + report, + onRequestDelete, +}: { + report: ReportMeta; + onRequestDelete: (r: ReportMeta) => void; +}) { + const router = useRouter(); + const timeAgo = formatRelative(report.created_at); + + return ( + router.push(`/reports/${report.id}`)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + router.push(`/reports/${report.id}`); + } + }} + className="group relative w-full cursor-pointer text-left hover:shadow-[0_6px_20px_rgba(0,0,0,0.08)]" + style={{ + background: "#ffffff", + border: "0.5px solid rgba(0,0,0,0.08)", + borderRadius: 12, + padding: "16px 18px", + paddingRight: 44, + boxShadow: "0 1px 3px rgba(0,0,0,0.04)", + transition: "box-shadow 0.15s", + }} + > + +

+ {report.title || report.query} +

+ + {report.title && ( +

+ {report.query} +

+ )} + +
+ {timeAgo} + {report.latest_char_count && ( + {Math.round(report.latest_char_count / 1000)}k chars + )} + {report.latest_version && ( + + v{report.latest_version} + + )} + + {researchIntensityLabel(report.strength)} + +
+
+ ); +} + + +export default function DashboardPage() { + const { status: authStatus, data: session } = useSession(); + const apiReady = + authStatus === "authenticated" && Boolean(session?.accessToken); + const router = useRouter(); + const queryClient = useQueryClient(); + const [query, setQuery] = useState(""); + const [barMode, setBarMode] = useState<"chat" | "research">("research"); + const [barExtended, setBarExtended] = useState(false); + const [barJobStrength, setBarJobStrength] = useState<1 | 2 | 3>(2); + const [barDebugMockResearch, setBarDebugMockResearch] = useState(false); + const [barModelId, setBarModelId] = useState(DEFAULT_CHAT_MODEL_ID); + const [deleteTarget, setDeleteTarget] = useState(null); + const [dashThreadId, setDashThreadId] = useState(null); + const [dashLaunch, setDashLaunch] = useState(null); + const [dashChatBusy, setDashChatBusy] = useState(false); + const [chatSidebarCollapsed, setChatSidebarCollapsed] = useState(false); + const [researchDockThreadId, setResearchDockThreadId] = useState(null); + const [researchDockReportId, setResearchDockReportId] = useState(null); + const [deleteThreadTarget, setDeleteThreadTarget] = useState(null); + + const { data: reportsData, isLoading: reportsQueryLoading } = useQuery({ + queryKey: ["reports"], + queryFn: () => reportsApi.list(), + enabled: apiReady, + }); + + const { data: threadsRaw, isLoading: threadsQueryLoading } = useQuery({ + queryKey: ["dashboard-threads"], + queryFn: () => threadsApi.list(50), + enabled: apiReady, + }); + + const { data: llmCatalog, isLoading: llmCatalogLoading } = useQuery({ + queryKey: ["llm-catalog"], + queryFn: () => llmApi.models(), + enabled: apiReady, + staleTime: 120_000, + }); + + const barModelSelectGroups = useMemo( + () => llmModelGroupsFromCatalog(llmCatalog?.models), + [llmCatalog?.models], + ); + + const showBarModelPicker = + apiReady && + !llmCatalogLoading && + (llmCatalog?.models?.length ?? 0) > 0; + + useEffect(() => { + const list = llmCatalog?.models ?? []; + const ids = new Set(list.map((m) => m.model_id)); + if (ids.size === 0) return; + if (!ids.has(barModelId)) { + setBarModelId(list[0]!.model_id); + } + }, [llmCatalog, barModelId]); + + const barChatModelReady = useMemo(() => { + if (!showBarModelPicker) return false; + return (llmCatalog?.models ?? []).some((m) => m.model_id === barModelId); + }, [showBarModelPicker, llmCatalog?.models, barModelId]); + + const reportsLoading = !apiReady || reportsQueryLoading; + const threadsLoading = !apiReady || threadsQueryLoading; + + const researchDockHeading = useMemo(() => { + if (!researchDockReportId || !reportsData?.items?.length) return null; + const r = reportsData.items.find((x) => x.id === researchDockReportId); + return r?.title?.trim() || r?.query?.trim() || null; + }, [researchDockReportId, reportsData?.items]); + + const barChatToggleLabel = + barMode === "chat" && query.trim() + ? truncateDisplayLabel(query.trim(), 28) + : "Chat"; + const barResearchToggleLabel = + barMode === "research" && query.trim() + ? truncateDisplayLabel(query.trim(), 28) + : "Research"; + + const createJobMutation = useMutation({ + mutationFn: () => + jobsApi.create( + query, + barJobStrength, + undefined, + barDebugMockResearch, + barModelId, + ), + onSuccess: (job) => { + setDashThreadId(null); + setDashLaunch(null); + setQuery(""); + router.push(`/reports/${job.report_id}?job=${job.job_id}`); + }, + }); + + const deleteReportMutation = useMutation({ + mutationFn: (id: string) => reportsApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["reports"] }); + setDeleteTarget(null); + }, + }); + + const deleteThreadMutation = useMutation({ + mutationFn: (id: string) => threadsApi.delete(id), + onSuccess: (_, deletedId) => { + queryClient.invalidateQueries({ queryKey: ["dashboard-threads"] }); + queryClient.invalidateQueries({ queryKey: ["report-default-thread"] }); + setDeleteThreadTarget(null); + if (dashThreadId === deletedId) { + setDashThreadId(null); + setDashLaunch(null); + } + }, + }); + + const handleBarSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault(); + const text = query.trim(); + if (!text) return; + if (barMode === "research") { + if (text.length < 10) return; + createJobMutation.mutate(); + return; + } + setDashChatBusy(true); + try { + const thread = await threadsApi.create(); + setDashThreadId(thread.id); + setDashLaunch({ + nonce: Date.now(), + message: text, + execution_mode: "chat", + chat_variant: barExtended ? "extended" : "standard", + research_strength: 5, + model_id: barModelId, + }); + setQuery(""); + queryClient.invalidateQueries({ queryKey: ["dashboard-threads"] }); + } catch (err) { + console.error("Failed to start chat thread", err); + } finally { + setDashChatBusy(false); + } + }, + [query, barMode, barExtended, barModelId, barJobStrength, createJobMutation, queryClient], + ); + + + const handleCloseResearchDock = useCallback(() => { + setResearchDockThreadId(null); + setResearchDockReportId(null); + }, []); + + const onLaunchConsumed = useCallback(() => { + setDashLaunch(null); + }, []); + + const handleCloseDashChat = useCallback(() => { + setDashThreadId(null); + setDashLaunch(null); + queryClient.invalidateQueries({ queryKey: ["dashboard-threads"] }); + }, [queryClient]); + + const handleSelectSidebarThread = useCallback( + (t: ThreadSummaryResponse) => { + setDashLaunch(null); + if (t.report_id) { + router.push(`/reports/${t.report_id}?thread=${t.id}`); + return; + } + setDashThreadId(t.id); + }, + [router], + ); + + const handleSidebarNewChat = useCallback(() => { + setDashThreadId(null); + setDashLaunch(null); + }, []); + + if (authStatus === "loading") { + return ( +
+
+
+ ); + } + + if (authStatus === "unauthenticated") { + router.push("/"); + return null; + } + + if ( + authStatus === "authenticated" && + (!session?.accessToken || session?.error) + ) { + return ; + } + + const reports = reportsData?.items || []; + + return ( +
+
+ setChatSidebarCollapsed((c) => !c)} + threads={threadsRaw ?? []} + selectedThreadId={dashThreadId} + onSelectThread={handleSelectSidebarThread} + onNewChat={handleSidebarNewChat} + onRequestDelete={setDeleteThreadTarget} + isLoading={threadsLoading} + /> + +
+ {dashThreadId ? ( + + ) : ( +
+
+
+

+ Recent Projects +

+ {reports.length} reports +
+ + {reportsLoading ? ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ ) : reports.length === 0 ? ( + +
+

+ Start your first research project +

+

+ Type a topic below and press enter +

+
+
+ ) : ( +
+ {reports.map((report) => ( + + ))} +
+ )} +
+
+
+
+
+ + +
+ + {showBarModelPicker ? ( +
+ Model + +
+ ) : null} + + {barMode === "chat" ? ( +
+ Extended + +
+ ) : ( +
+ Intensity +
+ {RESEARCH_INTENSITY_OPTIONS.map(({ tier, label }) => ( + + ))} +
+ {showDebugMockResearchControls(session?.user?.email) ? ( + + ) : null} +
+ )} +
+ +
+ setQuery(e.target.value)} + placeholder={ + barMode === "research" + ? "New report topic (min. 10 characters)…" + : "Message…" + } + className="min-w-0 flex-1 bg-transparent py-2 pl-2 text-base text-neutral-900 outline-none placeholder:text-neutral-400" + style={{ fontFamily: "var(--serif, 'Newsreader', Georgia, serif)" }} + /> + +
+
+
+
+ )} + +
+ + + {researchDockThreadId ? ( + + ) : null} + +
+ + { + if (!deleteReportMutation.isPending) { + deleteReportMutation.reset(); + setDeleteTarget(null); + } + }} + onConfirm={() => { + if (deleteTarget) deleteReportMutation.mutate(deleteTarget.id); + }} + /> + + { + if (!deleteThreadMutation.isPending) { + deleteThreadMutation.reset(); + setDeleteThreadTarget(null); + } + }} + onConfirm={() => { + if (deleteThreadTarget) deleteThreadMutation.mutate(deleteThreadTarget.id); + }} + /> +
+ ); +} diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/frontend/src/app/favicon.ico differ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..68f3ea0 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,137 @@ +@import "tailwindcss"; + +@theme { + /* App shell matches research / report editorial paper palette */ + --color-bg: #f7f5f0; + --color-card: #ffffff; + --color-border: #e5e2db; + --color-accent: #6366f1; + --color-text: #2a2824; + --color-muted: #5c5952; + --font-sans: var(--font-newsreader), 'Newsreader', Georgia, serif; + --font-serif: var(--font-newsreader), 'Newsreader', Georgia, serif; + --font-mono: var(--font-jetbrains-mono), 'JetBrains Mono', monospace; +} + +:root { + --shell-bg: #f7f5f0; + --shell-card: #ffffff; + --shell-border: #e5e2db; + --shell-accent: #6366f1; + --shell-text: #2a2824; + --shell-muted: #5c5952; + /* Editorial paper palette — mirrors render/html_report.py */ + --rpt-bg: #f7f5f0; + --rpt-bg2: #ffffff; + --rpt-bg3: #ece9e2; + --rpt-border: rgba(0,0,0,0.08); + --rpt-border-hi: rgba(0,0,0,0.14); + --rpt-text: #2a2824; + --rpt-text-dim: #5c5952; + --rpt-text-hi: #141210; + --rpt-accent: #1a6fd4; + --rpt-accent2: #0d8a5b; + --rpt-accent3: #c45c00; + --mono: var(--font-jetbrains-mono), 'JetBrains Mono', monospace; + --serif: var(--font-newsreader), 'Newsreader', Georgia, serif; +} + +/** + * Universal reset must live inside @layer base. An unlayered `* { padding: 0 }` after + * `@import "tailwindcss"` outranks Tailwind’s layered utilities in the cascade, which + * zeros horizontal padding from `px-*` / `p-*` across the app (buttons, menus, shells). + */ +@layer base { + *, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + } + html { + scroll-behavior: smooth; + } + body { + background: var(--shell-bg); + color: var(--rpt-text); + font-family: var(--serif); + font-size: 17px; + line-height: 1.75; + -webkit-font-smoothing: antialiased; + } +} + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #9ca3af; } + +::selection { background: #6366f1; color: white; } +.report-content ::selection { background: #1a6fd4; color: white; } + +/* KaTeX math display */ +.katex-display { + overflow-x: auto; overflow-y: hidden; + -webkit-overflow-scrolling: touch; + padding: 4px 0; + max-width: 100%; +} +.report-content .katex { color: var(--rpt-text-hi); } + +.chat-message-content .katex { + color: inherit; + font-size: 1.05em; +} +.chat-message-content .katex-display { + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + max-width: 100%; + margin: 0.65em 0; + padding: 4px 0; +} + +/* GFM tables (remark-gfm) — @layer base zeros padding on all elements */ +.chat-message-content .chat-table-wrap { + overflow-x: auto; + margin: 12px 0; + max-width: 100%; + -webkit-overflow-scrolling: touch; + border-radius: 8px; + border: 0.5px solid var(--rpt-border-hi); +} + +.chat-message-content .chat-table-wrap table { + width: 100%; + min-width: min(100%, 320px); + border-collapse: collapse; + margin: 0; + font-size: 15px; +} + +.chat-message-content .chat-table-wrap th, +.chat-message-content .chat-table-wrap td { + border: 0.5px solid var(--rpt-border); + padding: 10px 14px; + vertical-align: top; + text-align: left; + line-height: 1.55; + color: var(--rpt-text-hi); +} + +.chat-message-content .chat-table-wrap th { + background: var(--rpt-bg3); + font-family: var(--mono); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--rpt-text-dim); +} + +.chat-message-content .chat-table-wrap tbody tr:nth-child(even) td { + background: rgba(0, 0, 0, 0.02); +} + +.chat-message-content .chat-table-wrap tbody tr:hover td { + background: rgba(0, 0, 0, 0.035); +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..c5111d1 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,54 @@ +import type { Metadata } from "next"; +import { JetBrains_Mono, Newsreader } from "next/font/google"; +import "./globals.css"; +import { Providers } from "@/components/providers"; + +const newsreader = Newsreader({ + subsets: ["latin"], + weight: ["300", "400", "500"], + style: ["normal", "italic"], + variable: "--font-newsreader", +}); + +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + weight: ["400", "500"], + variable: "--font-jetbrains-mono", +}); + +export const metadata: Metadata = { + title: "Singularity", + description: "Deep research, at the speed of thought.", + icons: { + icon: [ + { url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" }, + { url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" }, + { url: "/favicon.ico" }, + { + url: "/android-chrome-192x192.png", + sizes: "192x192", + type: "image/png", + }, + { + url: "/android-chrome-512x512.png", + sizes: "512x512", + type: "image/png", + }, + ], + apple: "/apple-touch-icon.png", + }, + manifest: "/site.webmanifest", + other: { + "theme-color": "#f7f5f0", + }, +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..c02d0b4 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,190 @@ +"use client"; + +import { signIn, signOut, useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { motion } from "framer-motion"; +import { AppLogoMark } from "@/components/app-logo"; +import { publicApiBaseUrl } from "@/lib/public_api_base_url"; + +export default function LandingPage() { + const { status, data: session } = useSession(); + const router = useRouter(); + + useEffect(() => { + if (status !== "authenticated") return; + if (!session?.accessToken || session?.error) return; + router.push("/dashboard"); + }, [status, session?.accessToken, session?.error, router]); + + const backendBlocked = + status === "authenticated" && + (!session?.accessToken || Boolean(session?.error)); + + return ( +
+
+
+
+ + + {/* Logo / Brand */} + + +

+ Singularity +

+
+ + {/* Tagline */} + + Intelligence Emerges At This Point + + + {/* Philosophical quote — editorial pull-quote treatment */} + + + “ + +

+ I Think. Therefore, I Am. +

+
+ + René Descartes + +
+
+ + {backendBlocked && ( + +

Signed in with Google — API link missing

+

+ The Next.js server could not exchange your Google session for app tokens. Start the API, + set{" "} + + NEXT_PUBLIC_API_URL + {" "} + to where the browser should call FastAPI, and if the web app runs in Docker set{" "} + + INTERNAL_API_URL + {" "} + (e.g. http://host.docker.internal:8000) + so the Next.js server can reach the API. Ensure API{" "} + + GOOGLE_CLIENT_ID + {" "} + matches this app. +

+

+ Browser API base: {publicApiBaseUrl()} +

+

+ After the API is running with matching Google client config, sign out and use "Sign in with + Google" again so the app can exchange your session. +

+
+ +
+
+ )} + + {/* Sign in button */} + { + void signIn("google", { callbackUrl: "/dashboard" }); + }} + disabled={status === "loading" || backendBlocked} + className="mt-2 flex h-12 items-center gap-3 rounded-full bg-[#111827] px-7 text-base font-medium text-white shadow-sm transition-colors hover:bg-[#1f2937] disabled:opacity-50" + > + + + + + + + {status === "loading" ? "Connecting..." : "Sign in with Google"} + + + {/* Features hint */} + + {[ + "Research Reports", + "Chat", + "Explore", + "Learn", + ].map((feature) => ( + + + {feature} + + ))} + +
+
+ ); +} diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx new file mode 100644 index 0000000..e977972 --- /dev/null +++ b/frontend/src/app/profile/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { ArrowLeft } from "lucide-react"; +import { UserMenu } from "@/components/user-menu"; +import { AppLogoMark } from "@/components/app-logo"; +import { AccountReconnectPrompt } from "@/components/account_reconnect_prompt"; +import { ProfileLlmKeysSection } from "@/components/profile_llm_keys_section"; + +export default function ProfilePage() { + const { status: authStatus, data: session } = useSession(); + const apiReady = + authStatus === "authenticated" && Boolean(session?.accessToken); + const router = useRouter(); + + if (authStatus === "loading") { + return ( +
+
+
+ ); + } + + if (authStatus === "unauthenticated") { router.push("/"); return null; } + + if ( + authStatus === "authenticated" && + (!session?.accessToken || session?.error) + ) { + return ; + } + + return ( +
+
+
+ +

+ API Keys +

+
+ +
+ + Singularity +
+ +
+ +
+
+ +
+ +
+
+ ); +} diff --git a/frontend/src/app/reports/[id]/page.tsx b/frontend/src/app/reports/[id]/page.tsx new file mode 100644 index 0000000..ff379e2 --- /dev/null +++ b/frontend/src/app/reports/[id]/page.tsx @@ -0,0 +1,851 @@ +"use client"; + +import "@/styles/report-content.css"; +import "katex/dist/katex.min.css"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import type { RawResearchActivity } from "@/lib/research_activity_presenter"; +import { phaseStoryboardContext } from "@/lib/research_activity_presenter"; +import { + ResearchOperationsFeed, + RESEARCH_ACTIVITY_CAP, +} from "@/components/report/ResearchOperationsFeed"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { motion, AnimatePresence } from "framer-motion"; +import { ArrowLeft, Download } from "lucide-react"; +import { reportsApi, threadsApi, jobsApi, ApiError } from "@/lib/api"; +import { consumeSSE } from "@/lib/sse"; +import { researchIntensityLabel } from "@/lib/utils"; +import { ReportViewer, type TOCEntry } from "@/components/report/ReportViewer"; +import { ReportTOC } from "@/components/report/ReportTOC"; +import { SelectionToolbar } from "@/components/report/SelectionToolbar"; +import { PatchModal } from "@/components/report/PatchModal"; +import { ChatPanel } from "@/components/chat/ChatPanel"; +import { AppLogoMark } from "@/components/app-logo"; +import { UserMenu } from "@/components/user-menu"; +import { AccountReconnectPrompt } from "@/components/account_reconnect_prompt"; + +export default function ReportViewPage() { + const params = useParams(); + const router = useRouter(); + const searchParams = useSearchParams(); + const queryClient = useQueryClient(); + const reportId = params.id as string; + const threadParam = searchParams.get("thread"); + const { status: authStatus, data: session } = useSession(); + const apiReady = + authStatus === "authenticated" && Boolean(session?.accessToken); + + const jobParam = searchParams.get("job"); + + const [tocEntries, setTocEntries] = useState([]); + const [selectedText, setSelectedText] = useState(""); + const [patchModalOpen, setPatchModalOpen] = useState(false); + const [patchConflict, setPatchConflict] = useState(false); + + const [trackedJobStatus, setTrackedJobStatus] = useState(null); + const [trackedJobPhase, setTrackedJobPhase] = useState(null); + const [trackedJobDesc, setTrackedJobDesc] = useState(null); + const [trackedJobError, setTrackedJobError] = useState(null); + const [trackedJobStartedAt, setTrackedJobStartedAt] = useState(null); + const [activityLog, setActivityLog] = useState([]); + + const { data: reportMeta, isLoading: metaLoading, isFetching: metaFetching } = useQuery({ + queryKey: ["report", reportId], + queryFn: () => reportsApi.get(reportId), + enabled: !!reportId && apiReady, + refetchInterval: (query) => { + const d = query.state.data; + if (!d || d.latest_version != null) return false; + return 4000; + }, + }); + + const { data: versionContent, isLoading: contentLoading } = useQuery({ + queryKey: ["report-content", reportId, reportMeta?.latest_version], + queryFn: () => reportsApi.getVersionContent(reportId, reportMeta!.latest_version!), + enabled: !!reportMeta?.latest_version && apiReady, + }); + + const { data: versionsData } = useQuery({ + queryKey: ["report-versions", reportId], + queryFn: () => reportsApi.getVersions(reportId), + enabled: !!reportId && apiReady, + }); + + const { data: defaultThread } = useQuery({ + queryKey: ["report-default-thread", reportId], + queryFn: () => reportsApi.defaultThread(reportId), + enabled: !!reportId && apiReady, + }); + + const { + data: explicitThreadBundle, + isError: explicitThreadError, + } = useQuery({ + queryKey: ["thread-detail", threadParam], + queryFn: () => threadsApi.get(threadParam!), + enabled: !!reportId && !!threadParam && apiReady, + retry: false, + }); + + const activeThreadId = useMemo(() => { + if ( + threadParam && + explicitThreadBundle?.thread.report_id === reportId + ) { + return threadParam; + } + if (threadParam && explicitThreadError) { + return defaultThread?.id ?? null; + } + if (defaultThread) return defaultThread.id; + return null; + }, [ + threadParam, + explicitThreadBundle, + explicitThreadError, + reportId, + defaultThread, + ]); + + useEffect(() => { + if (!jobParam || !apiReady) return; + let cancelled = false; + + async function trackJob() { + try { + const initial = await jobsApi.get(jobParam!); + if (cancelled) return; + + setTrackedJobStatus(initial.status); + setTrackedJobPhase(initial.current_phase); + setTrackedJobStartedAt(initial.started_at); + + if (initial.status === "done") { + queryClient.invalidateQueries({ queryKey: ["report", reportId] }); + queryClient.invalidateQueries({ queryKey: ["report-content", reportId] }); + router.replace(`/reports/${reportId}`); + return; + } + if (initial.status === "failed" || initial.status === "cancelled") { + setTrackedJobError(initial.error_detail); + return; + } + + const session = await import("next-auth/react").then((m) => m.getSession()); + const token = session?.accessToken as string | undefined; + const url = jobsApi.eventsUrl(jobParam!, token); + + try { + for await (const evt of consumeSSE(url, token)) { + if (cancelled) break; + const data = JSON.parse(evt.data); + if (evt.event === "job_activity") { + const row = data as Record; + const id = evt.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + const kind = typeof row.kind === "string" ? row.kind : "unknown"; + const phase = typeof row.phase === "string" ? row.phase : "planning"; + const entry: RawResearchActivity = { + id, + kind, + phase, + meta: + row.meta && typeof row.meta === "object" && row.meta !== null + ? (row.meta as Record) + : undefined, + elapsed_ms: typeof row.elapsed_ms === "number" ? row.elapsed_ms : undefined, + }; + setActivityLog((prev) => { + const next = [...prev, entry]; + if (next.length > RESEARCH_ACTIVITY_CAP) { + return next.slice(-RESEARCH_ACTIVITY_CAP); + } + return next; + }); + continue; + } + if (data.phase !== undefined) setTrackedJobPhase(data.phase); + if (typeof data.description === "string") setTrackedJobDesc(data.description); + if (data.status) setTrackedJobStatus(data.status); + + if (evt.event === "job_done") { + setTrackedJobStatus("done"); + queryClient.invalidateQueries({ queryKey: ["report", reportId] }); + queryClient.invalidateQueries({ queryKey: ["report-content", reportId] }); + router.replace(`/reports/${reportId}`); + break; + } + if (evt.event === "job_error") { + setTrackedJobStatus("failed"); + if (data.error) setTrackedJobError(data.error); + break; + } + if (evt.event === "job_cancelled") { + setTrackedJobStatus("cancelled"); + break; + } + } + } catch { + // SSE dropped — fall back to polling below + } + + if (cancelled) return; + + // Polling fallback: kick in if SSE closed without terminal event + const intervalId = setInterval(async () => { + if (cancelled) { clearInterval(intervalId); return; } + try { + const j = await jobsApi.get(jobParam!); + if (cancelled) return; + if (j.current_phase) setTrackedJobPhase(j.current_phase); + if (j.error_detail) setTrackedJobError(j.error_detail); + setTrackedJobStatus(j.status); + if (j.status === "done") { + clearInterval(intervalId); + queryClient.invalidateQueries({ queryKey: ["report", reportId] }); + queryClient.invalidateQueries({ queryKey: ["report-content", reportId] }); + router.replace(`/reports/${reportId}`); + } else if (j.status === "failed" || j.status === "cancelled") { + clearInterval(intervalId); + } + } catch { /* ignore */ } + }, 3000); + return () => clearInterval(intervalId); + } catch { + // ignore initial fetch failure + } + } + + trackJob(); + return () => { cancelled = true; }; + }, [jobParam, apiReady, reportId, queryClient, router]); + + const isLoading = metaLoading || contentLoading; + const isEditable = !!versionContent && !isLoading; + + const handleSelection = useCallback((text: string, _headingSlug: string | null) => { + setSelectedText(text); + }, []); + + const handlePatch = useCallback(async (instruction: string) => { + if (!versionContent) throw new Error("No version loaded"); + try { + await reportsApi.patch(reportId, versionContent.version_num, { + selected_text: selectedText, + instruction, + if_match: versionContent.etag, + }); + } catch (e) { + if (e instanceof ApiError && e.status === 409) { + setPatchConflict(true); + } + throw e; + } + queryClient.invalidateQueries({ queryKey: ["report", reportId] }); + queryClient.invalidateQueries({ queryKey: ["report-content", reportId] }); + queryClient.invalidateQueries({ queryKey: ["report-versions", reportId] }); + setSelectedText(""); + setPatchModalOpen(false); + }, [versionContent, reportId, selectedText, queryClient]); + + const handleExport = useCallback(() => { + if (!versionContent) return; + const url = reportsApi.exportUrl(reportId, versionContent.version_num, "html"); + window.open(url, "_blank"); + }, [reportId, versionContent]); + + const handleBackDashboard = useCallback(() => { + router.push("/dashboard"); + }, [router]); + + if (authStatus === "loading") { + return ( +
+
+
+ ); + } + + if (authStatus === "unauthenticated") { + router.push("/"); + return null; + } + + if ( + authStatus === "authenticated" && + (!session?.accessToken || session?.error) + ) { + return ; + } + + return ( +
+
+
+ + {reportMeta && ( +

+ {reportMeta.title || reportMeta.query} +

+ )} +
+ +
+ + Singularity +
+ +
+ {versionContent && ( + + v{versionContent.version_num} + {versionsData && versionsData.versions.length > 1 && ( + /{versionsData.versions.length} + )} + + )} + + + + +
+
+ + + {patchConflict && ( + + Document was updated — reload to see the latest version before editing. + + + )} + + +
+
+ {jobParam && trackedJobStatus && trackedJobStatus !== "done" && !versionContent ? ( + router.push("/dashboard")} + /> + ) : isLoading ? ( + + ) : versionContent ? ( +
+ + +
+
+
Research Report
+

+ {reportMeta?.title || reportMeta?.query} +

+
+ {reportMeta?.strength != null && ( + + Intensity: {researchIntensityLabel(reportMeta.strength)} ( + {reportMeta.strength}) + + )} + · + {versionContent.char_count.toLocaleString()} chars + · + + {new Date(reportMeta?.created_at || "").toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + })} + +
+
+ +
+ + setPatchModalOpen(true)} + /> +
+
+
+ ) : ( +
+

+ {reportMeta?.latest_version == null + ? "No report version yet. If generation is still running, this page will update automatically. If it failed (e.g. Qdrant unreachable), fix your worker env or start Qdrant, then try a new research job." + : "No content available"} +

+ {reportMeta?.latest_version == null && metaFetching ? ( +
+ ) : null} + +
+ )} +
+ +
+ {activeThreadId ? ( + + ) : ( +
+
+

Loading chat…

+
+ )} +
+
+ + setPatchModalOpen(false)} + onSubmit={handlePatch} + /> +
+ ); +} + +const PHASE_LABELS: Record = { + planning: "Structuring report", + retrieval: "Gathering sources", + writing: "Drafting content", + polish: "Polishing markdown", +}; + +const PHASE_ORDER = ["planning", "retrieval", "writing", "polish"]; + +function formatRunningFor(iso: string | null): string | null { + if (!iso) return null; + const t = new Date(iso).getTime(); + if (Number.isNaN(t)) return null; + const ms = Date.now() - t; + if (ms < 0) return "0s"; + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + const rs = s % 60; + if (m < 1) return `${rs}s`; + if (m < 60) return `${m}m ${rs.toString().padStart(2, "0")}s`; + const h = Math.floor(m / 60); + const rm = m % 60; + return `${h}h ${rm}m`; +} + +function ReportJobProgress({ + status, + phase, + description, + error, + query, + startedAt, + activityEntries, + onBack, +}: { + status: string; + phase: string | null; + description: string | null; + error: string | null; + query: string | null; + startedAt: string | null; + activityEntries: RawResearchActivity[]; + onBack: () => void; +}) { + const isFailed = status === "failed" || status === "cancelled"; + const activeIdx = phase ? PHASE_ORDER.indexOf(phase) : -1; + const isRunning = !isFailed && (status === "pending" || status === "running"); + const [, setTick] = useState(0); + + useEffect(() => { + if (!isRunning || !startedAt) return; + const id = window.setInterval(() => setTick((x) => x + 1), 1000); + return () => window.clearInterval(id); + }, [isRunning, startedAt]); + + const runningFor = isRunning ? formatRunningFor(startedAt) : null; + const liveLine = phaseStoryboardContext(phase); + + return ( +
+
+
+ {query && ( +
+
+ Research query +
+

+ {query} +

+
+ )} + + {isRunning && runningFor && ( +
+ + Running for {runningFor} +
+ )} + +

+ {liveLine} +

+ +
+ {PHASE_ORDER.map((p, i) => { + const isDone = activeIdx > i || (isFailed && activeIdx >= i); + const isActive = i === activeIdx && !isFailed; + const isPending = i > activeIdx; + + return ( +
+
+ {isDone ? ( + + + + ) : isActive ? ( +
+ ) : null} +
+
+
+ {PHASE_LABELS[p]} +
+ {isActive && description && ( +
+ {description} +
+ )} +
+
+ ); + })} +
+ + {isFailed && ( +
+
+ {status === "cancelled" ? "Job cancelled" : "Generation failed"} +
+ {error && ( +
+ {error.length > 400 ? `${error.slice(0, 400)}…` : error} +
+ )} + +
+ )} + + {!isFailed && (status === "pending" || status === "running") && activeIdx === -1 && ( +
+
+ Queued — waiting for worker… +
+ )} +
+ +
+ +
+
+
+ ); +} + +function LoadingSkeleton() { + return ( +
+
+
+
+
+
+ {[100, 85, 95, 75, 90, 80, 92, 70, 88].map((w, i) => ( +
+ ))} +
+
+ ); +} diff --git a/frontend/src/assets/singularity-logo.png b/frontend/src/assets/singularity-logo.png new file mode 100644 index 0000000..30d5abf Binary files /dev/null and b/frontend/src/assets/singularity-logo.png differ diff --git a/frontend/src/auth.ts b/frontend/src/auth.ts new file mode 100644 index 0000000..e19d611 --- /dev/null +++ b/frontend/src/auth.ts @@ -0,0 +1,258 @@ +import NextAuth from "next-auth"; +import Google from "next-auth/providers/google"; +import type { NextAuthConfig } from "next-auth"; + +import { + normalizeBackendUrlForNodeFetch, + publicApiBaseUrl, +} from "@/lib/public_api_base_url"; + +/** True when fetch failed before an HTTP status (network / TLS / reset). */ +function isTransientNodeFetchError(e: unknown): boolean { + const msg = e instanceof Error ? e.message : String(e); + let cause: unknown = + e && typeof e === "object" && "cause" in e + ? (e as { cause?: unknown }).cause + : undefined; + const parts = [msg]; + for (let i = 0; i < 3 && cause instanceof Error; i++) { + parts.push(cause.message, String((cause as Error & { code?: string }).code ?? "")); + cause = "cause" in cause ? (cause as { cause?: unknown }).cause : undefined; + } + const s = parts.join(" ").toLowerCase(); + return ( + s.includes("econnreset") || + s.includes("econnrefused") || + s.includes("etimedout") || + s.includes("socket hang up") || + s.includes("fetch failed") + ); +} + +async function fetchWithTransientRetry( + url: string, + init: RequestInit, + attempts = 2, +): Promise { + let last: unknown; + for (let i = 0; i < attempts; i++) { + try { + return await fetch(url, init); + } catch (e) { + last = e; + if (!isTransientNodeFetchError(e) || i === attempts - 1) { + throw e; + } + } + } + throw last; +} + +/** True when Node runtime (not Edge) and `/.dockerenv` exists — avoids `fs` on Edge middleware bundles. */ +function isDockerContainerNode(): boolean { + if (typeof process === "undefined" || !process.versions?.node) return false; + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { existsSync } = require("node:fs") as typeof import("node:fs"); + return existsSync("/.dockerenv"); + } catch { + return false; + } +} + +/** + * URL the Next.js server uses for `/api/v1/auth/*` (JWT callback runs on the server). + * - Prefer INTERNAL_API_URL when set (Docker / split deploy). + * - If the process runs inside Docker and the public URL still points at localhost, + * rewrite the host to host.docker.internal so the container can reach the host API. + */ +function resolveBackendFetchBase(): string { + const raw = process.env.INTERNAL_API_URL; + if (typeof raw === "string") { + const t = raw.trim().replace(/\/+$/, ""); + if (t.length > 0) { + return normalizeBackendUrlForNodeFetch(t); + } + } + + const pub = publicApiBaseUrl(); + if (isDockerContainerNode()) { + try { + const u = new URL(pub); + if (u.hostname === "localhost" || u.hostname === "127.0.0.1") { + u.hostname = "host.docker.internal"; + return u.toString().replace(/\/+$/, ""); + } + } catch { + /* ignore */ + } + return "http://host.docker.internal:8000"; + } + + return normalizeBackendUrlForNodeFetch(pub); +} + +function applyBackendTokenPayload( + token: import("next-auth/jwt").JWT, + data: { access_token?: string; refresh_token?: string; expires_in?: number }, +) { + if (data.access_token) token.accessToken = data.access_token; + if (data.refresh_token) token.refreshToken = data.refresh_token; + const sec = + typeof data.expires_in === "number" && Number.isFinite(data.expires_in) + ? data.expires_in + : 900; + token.accessTokenExpires = Date.now() + sec * 1000; + delete token.error; +} + +type BackendTokenPayload = { + access_token?: string; + refresh_token?: string; + expires_in?: number; +}; + +/** + * Refresh rotation is single-use on the server; concurrent JWT callbacks must share + * one in-flight POST. Store the map on `globalThis` so Next.js dev HMR does not reset + * the map mid-flight (which previously allowed a second refresh with an already-rotated + * token and triggered family-wide revocation). + */ +const REFRESH_INFLIGHT_KEY = "__singularity_auth_refresh_inflight_v1" as const; + +function getRefreshInflightMap(): Map> { + const g = globalThis as unknown as Record< + string, + Map> | undefined + >; + if (!g[REFRESH_INFLIGHT_KEY]) { + g[REFRESH_INFLIGHT_KEY] = new Map(); + } + return g[REFRESH_INFLIGHT_KEY]!; +} + +type BackendRotateResult = + | { kind: "ok"; data: BackendTokenPayload } + | { kind: "auth_error" } + | { kind: "transient" }; + +async function rotateBackendRefresh(refreshToken: string): Promise { + const inflight = getRefreshInflightMap(); + let pending = inflight.get(refreshToken); + if (!pending) { + pending = (async (): Promise => { + try { + const res = await fetchWithTransientRetry( + `${resolveBackendFetchBase()}/api/v1/auth/refresh`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refresh_token: refreshToken }), + }, + ); + if (!res.ok) { + // Only treat definitive auth failures as "log out". 5xx / network-ish codes + // leave refresh + access cookies intact so the next JWT tick can retry. + if (res.status === 401 || res.status === 403) { + return { kind: "auth_error" }; + } + return { kind: "transient" }; + } + const data = (await res.json()) as BackendTokenPayload; + return { kind: "ok", data }; + } catch { + return { kind: "transient" }; + } finally { + inflight.delete(refreshToken); + } + })(); + inflight.set(refreshToken, pending); + } + return pending; +} + +export const authConfig: NextAuthConfig = { + providers: [ + Google({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), + ], + callbacks: { + async jwt({ token, account, user }) { + if (user?.email) { + token.email = user.email; + } + if (account?.id_token) { + delete token.error; + try { + const apiBase = resolveBackendFetchBase(); + const res = await fetchWithTransientRetry(`${apiBase}/api/v1/auth/google`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id_token: account.id_token }), + }); + if (res.ok) { + const data = await res.json(); + applyBackendTokenPayload(token, data); + } else { + const errBody = await res.text(); + console.error("Backend auth exchange failed:", res.status, errBody); + token.error = "BackendSignInError"; + } + } catch (e) { + console.error("Backend auth exchange error:", e); + token.error = "BackendSignInError"; + } + } + + const expRaw = token.accessTokenExpires; + const exp = expRaw != null ? Number(expRaw) : NaN; + const expiryTrusted = Number.isFinite(exp); + const accessMissing = !token.accessToken; + const accessTimedOut = + expiryTrusted && Date.now() > exp - 60_000; + const shouldRefreshBackend = + Boolean(token.refreshToken) && (accessMissing || accessTimedOut); + + if (shouldRefreshBackend && typeof token.refreshToken === "string") { + const result = await rotateBackendRefresh(token.refreshToken); + if (result.kind === "ok") { + applyBackendTokenPayload(token, result.data); + } else if (result.kind === "auth_error") { + console.error("Token refresh rejected — session cleared"); + delete token.accessToken; + delete token.accessTokenExpires; + delete token.refreshToken; + token.error = "RefreshAccessTokenError"; + } else { + console.warn( + "[auth] Refresh temporarily unavailable (backend or network); keeping session for retry", + ); + } + } + + return token; + }, + async session({ session, token }) { + const email = + session.user?.email ?? (typeof token.email === "string" ? token.email : undefined); + return { + ...session, + user: session.user + ? { ...session.user, ...(email ? { email } : {}) } + : session.user, + accessToken: token.accessToken ?? undefined, + error: token.error, + }; + }, + }, + pages: { + signIn: "/", + }, + secret: process.env.NEXTAUTH_SECRET, + trustHost: true, + debug: process.env.NODE_ENV === "development", +}; + +export const { handlers, auth, signIn, signOut } = NextAuth(authConfig); diff --git a/frontend/src/components/account_reconnect_prompt.tsx b/frontend/src/components/account_reconnect_prompt.tsx new file mode 100644 index 0000000..0ccc7c9 --- /dev/null +++ b/frontend/src/components/account_reconnect_prompt.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState } from "react"; +import { signOut, useSession } from "next-auth/react"; + +import { publicApiBaseUrl } from "@/lib/public_api_base_url"; + +/** + * Shown when NextAuth is authenticated but the app has no usable backend JWT + * (exchange or refresh failed). Offers retry and explicit sign-out instead of + * immediately signing out (which caused a redirect loop with the landing page). + */ +export function AccountReconnectPrompt() { + const { update } = useSession(); + const [busy, setBusy] = useState(false); + + return ( +
+
+

Can't finish sign-in

+

+ You're signed in with Google, but the Next.js server could not reach + FastAPI to exchange your session for app tokens. Start the API and check env: the browser + uses{" "} + NEXT_PUBLIC_API_URL + ; sign-in also needs{" "} + INTERNAL_API_URL when + the web app runs in Docker (e.g.{" "} + + http://host.docker.internal:8000 + + ), because localhost{" "} + inside the container is not your host machine. If Next.js runs in Docker and your API is on + the host, the app also tries{" "} + + host.docker.internal + {" "} + automatically when INTERNAL_API_URL{" "} + is unset — set it explicitly if your API is another container (e.g. compose service name). +

+

+ Browser API base: {publicApiBaseUrl()} +

+
+
+ + +
+
+ ); +} diff --git a/frontend/src/components/app-logo.tsx b/frontend/src/components/app-logo.tsx new file mode 100644 index 0000000..8b06dba --- /dev/null +++ b/frontend/src/components/app-logo.tsx @@ -0,0 +1,28 @@ +"use client"; + +import Image from "next/image"; +import { cn } from "@/lib/cn"; +import singularityLogo from "@/assets/singularity-logo.png"; + +type AppLogoMarkProps = { + className?: string; + priority?: boolean; +}; + +/** + * Geometric Singularity mark. Imported from `src/assets` so the built URL + * includes a content hash — replace `src/assets/singularity-logo.png` when + * updating the logo (avoids stale `/_next/image` cache on `/public` paths). + */ +export function AppLogoMark({ className, priority }: AppLogoMarkProps) { + return ( + Singularity logo + ); +} diff --git a/frontend/src/components/byok_reminder_toasts.tsx b/frontend/src/components/byok_reminder_toasts.tsx new file mode 100644 index 0000000..9b5a37e --- /dev/null +++ b/frontend/src/components/byok_reminder_toasts.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useSession } from "next-auth/react"; +import { useEffect } from "react"; +import { toast } from "sonner"; +import { usersApi } from "@/lib/api"; + +const BYOK_TOAST_ID = "byok-required"; + +/** + * Purpose: While the user is signed in but has no LLM keys saved, show a persistent top toast + * directing them to Profile. Dismisses automatically once at least one key exists. + */ +export function ByokReminderToasts() { + const { data: session, status } = useSession(); + const accessReady = + status === "authenticated" && Boolean((session as { accessToken?: string } | null)?.accessToken); + + const { data, isLoading, isFetched } = useQuery({ + queryKey: ["llm-credentials"], + queryFn: () => usersApi.llmCredentials(), + enabled: accessReady, + }); + + useEffect(() => { + if (!accessReady || !isFetched || isLoading) return; + const n = data?.credentials?.length ?? 0; + if (n > 0) { + toast.dismiss(BYOK_TOAST_ID); + return; + } + toast.error("Add an LLM API key in Profile before using models.", { + id: BYOK_TOAST_ID, + duration: Infinity, + action: { + label: "Profile", + onClick: () => { + window.location.href = "/profile"; + }, + }, + }); + }, [accessReady, isFetched, isLoading, data?.credentials?.length]); + + return null; +} diff --git a/frontend/src/components/chat/ChatModelPicker.tsx b/frontend/src/components/chat/ChatModelPicker.tsx new file mode 100644 index 0000000..9d3d379 --- /dev/null +++ b/frontend/src/components/chat/ChatModelPicker.tsx @@ -0,0 +1,89 @@ +"use client"; + +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronDown } from "lucide-react"; +import type { LlmCatalogModel } from "@/lib/api"; +import { cn } from "@/lib/cn"; + +const PROVIDER_LABELS: Record = { + grok: "xAI Grok", + gemini: "Google Gemini", + deepseek: "DeepSeek", +}; + +export type ChatModelPickerGroup = readonly [string, LlmCatalogModel[]]; + +interface ChatModelPickerProps { + /** Ordered provider groups from the filtered BYOK catalog. */ + groups: ChatModelPickerGroup[]; + value: string; + onChange: (modelId: string) => void; + disabled?: boolean; +} + +/** + * Purpose: Custom model selector for chat — grouped by provider, keyboard-friendly via Radix. + * Inputs: Eligible models (already filtered server-side), current model_id, change handler. + * Outputs: Trigger + dropdown. Parent hides this when the catalog is empty. + */ +export function ChatModelPicker({ + groups, + value, + onChange, + disabled = false, +}: ChatModelPickerProps) { + const flat = groups.flatMap(([, items]) => items); + const current = flat.find((m) => m.model_id === value); + + if (flat.length === 0) return null; + + return ( + + + + + + + {groups.map(([prov, items]) => ( +
+ + {PROVIDER_LABELS[prov] ?? prov} + + {items.map((m) => ( + onChange(m.model_id)} + > + {m.display_name} + {m.model_id === value ? ( + + ) : null} + + ))} +
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/components/chat/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx new file mode 100644 index 0000000..44158ab --- /dev/null +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -0,0 +1,703 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { motion } from "framer-motion"; +import { X, Send, Loader2 } from "lucide-react"; +import { getSession, useSession } from "next-auth/react"; +import { + threadsApi, + llmApi, + DEFAULT_CHAT_MODEL_ID, + type MessageResponse, +} from "@/lib/api"; +import { showDebugMockResearchControls } from "@/lib/debug_research_mock"; +import { cn } from "@/lib/cn"; +import { llmModelGroupsFromCatalog } from "@/lib/llm_model_groups"; +import { + truncateDisplayLabel, + researchIntensityLabel, + RESEARCH_INTENSITY_OPTIONS, + clampResearchIntensityTier, +} from "@/lib/utils"; +import { ChatModelPicker } from "./ChatModelPicker"; +import { MessageBubble } from "./MessageBubble"; +import { StreamingMessage } from "./StreamingMessage"; + +/** Dedupes bootstrap sends across React Strict Mode double effect runs. */ +const launchedBootstrapKeys = new Set(); + +/** Matches split-pane surface (`bg-white`) for a soft radial scrim behind floating chrome. */ +const SPLIT_PANE_SCRIM_RGB = "255,255,255"; + +type ExecutionMode = "chat" | "research"; + +export type ChatPanelLaunchPayload = { + nonce: number; + message: string; + execution_mode: ExecutionMode; + chat_variant: "standard" | "extended"; + research_strength: number; + /** When set (e.g. from dashboard bar), used for the bootstrap send model_id. */ + model_id?: string; +}; + +type SendOverrides = { + content: string; + execution_mode: ExecutionMode; + chat_variant: "standard" | "extended"; + research_strength: number; + model_id?: string; + debug_mock?: boolean; + onComplete?: () => void; +}; + +interface ChatPanelProps { + threadId: string; + reportContent?: string; + /** When true, show report-scoped labels even if report markdown is not loaded (e.g. dashboard research dock). */ + reportScope?: boolean; + /** Report title (or primary heading) for pane labels when this thread is tied to a report. */ + reportHeading?: string | null; + onClose: () => void; + /** overlay = report page slide-over; dock = dashboard right rail; main = dashboard primary; split = report page two-column */ + placement?: "overlay" | "dock" | "main" | "split"; + launchWith?: ChatPanelLaunchPayload | null; + onLaunchConsumed?: () => void; + /** Seeds the response model when the panel mounts (e.g. dashboard bar selection). */ + initialModelId?: string | null; +} + +/** + * Purpose: Thread UI with pill composer (chat vs research, extended, intensity) and optional + * bootstrap of the first turn when opened from the dashboard bar. + */ +export function ChatPanel({ + threadId, + reportContent, + reportScope = false, + reportHeading = null, + onClose, + placement = "overlay", + launchWith, + onLaunchConsumed, + initialModelId = null, +}: ChatPanelProps) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [streaming, setStreaming] = useState(false); + const [streamingTokens, setStreamingTokens] = useState(""); + const [error, setError] = useState(null); + const [executionMode, setExecutionMode] = useState("chat"); + const [extendedOn, setExtendedOn] = useState(false); + const [researchStrength, setResearchStrength] = useState<1 | 2 | 3>(2); + const [debugMockResearch, setDebugMockResearch] = useState(false); + const [selectedModelId, setSelectedModelId] = useState( + () => initialModelId?.trim() || DEFAULT_CHAT_MODEL_ID, + ); + const [statusLine, setStatusLine] = useState(null); + const bottomRef = useRef(null); + + const chatVariant = extendedOn ? "extended" : "standard"; + const standalone = !reportContent && !reportScope; + /** Report Q&A follow-ups: chat agent only (no research runs from this thread). */ + const followUpChatOnly = !standalone; + + const firstUserPaneLabel = useMemo(() => { + const m = messages.find((x) => x.role === "user"); + return m ? truncateDisplayLabel(m.content, 40) : ""; + }, [messages]); + + const reportPaneLabel = useMemo(() => { + if (standalone) return ""; + const h = reportHeading?.trim(); + return h ? truncateDisplayLabel(h, 40) : ""; + }, [standalone, reportHeading]); + + const headerTitle = standalone + ? firstUserPaneLabel || "Chat" + : reportPaneLabel || firstUserPaneLabel || "Report"; + + const chatToggleLabel = firstUserPaneLabel || "Chat"; + const researchToggleLabel = standalone + ? "Research" + : reportPaneLabel || "Research"; + + const { data: session, status: sessionStatus } = useSession(); + const accessReady = + sessionStatus === "authenticated" && Boolean(session?.accessToken); + + const { data: llmCatalog, isLoading: llmCatalogLoading } = useQuery({ + queryKey: ["llm-catalog"], + queryFn: () => llmApi.models(), + enabled: accessReady, + staleTime: 120_000, + }); + + const showModelPicker = + accessReady && + !llmCatalogLoading && + (llmCatalog?.models?.length ?? 0) > 0; + + const hasSelectableModel = useMemo(() => { + if (!showModelPicker) return false; + const list = llmCatalog?.models ?? []; + return list.some((m) => m.model_id === selectedModelId); + }, [showModelPicker, llmCatalog?.models, selectedModelId]); + + const modelSelectGroups = useMemo( + () => llmModelGroupsFromCatalog(llmCatalog?.models), + [llmCatalog?.models], + ); + + useEffect(() => { + const list = llmCatalog?.models ?? []; + const ids = new Set(list.map((m) => m.model_id)); + if (ids.size === 0) return; + if (!ids.has(selectedModelId)) { + setSelectedModelId(list[0]!.model_id); + } + }, [llmCatalog, selectedModelId]); + + useEffect(() => { + const pref = initialModelId?.trim(); + if (!pref) return; + const list = llmCatalog?.models ?? []; + if (list.some((m) => m.model_id === pref)) { + setSelectedModelId(pref); + } + }, [initialModelId, llmCatalog?.models]); + + useEffect(() => { + if (!threadId || !accessReady) return; + let cancelled = false; + threadsApi + .get(threadId) + .then((data) => { + if (cancelled) return; + setMessages(data.messages); + }) + .catch((e) => { + if (!cancelled) console.error("Failed to load thread messages", e); + }); + return () => { + cancelled = true; + }; + }, [threadId, accessReady]); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages, streamingTokens]); + + useEffect(() => { + if (followUpChatOnly) setExecutionMode("chat"); + }, [followUpChatOnly, threadId]); + + const runSend = useCallback( + async (overrides?: SendOverrides) => { + const content = (overrides?.content ?? input).trim(); + const catalogIds = new Set((llmCatalog?.models ?? []).map((m) => m.model_id)); + if (!content || streaming) return; + if ( + !accessReady || + llmCatalogLoading || + catalogIds.size === 0 || + !catalogIds.has(selectedModelId) + ) { + return; + } + const execMode = followUpChatOnly + ? "chat" + : overrides?.execution_mode ?? executionMode; + const variant = overrides?.chat_variant ?? chatVariant; + const strength = clampResearchIntensityTier( + overrides?.research_strength ?? researchStrength, + ); + const modelId = overrides?.model_id ?? selectedModelId; + const useDebugMock = + execMode === "research" && + !followUpChatOnly && + (overrides?.debug_mock ?? debugMockResearch); + if (!overrides) setInput(""); + setStreaming(true); + setStreamingTokens(""); + setError(null); + setStatusLine( + execMode === "research" + ? `Running research (${researchIntensityLabel(strength)} intensity)…` + : null, + ); + + const userMsg: MessageResponse = { + id: `tmp-${Date.now()}`, + role: "user", + content, + token_count: null, + created_at: new Date().toISOString(), + }; + setMessages((prev) => [...prev, userMsg]); + + try { + const session = await getSession(); + const token = (session as any)?.accessToken; + const url = threadsApi.sendMessageUrl(threadId); + + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + content, + execution_mode: execMode, + chat_variant: variant, + research_strength: strength, + model_id: modelId, + ...(useDebugMock ? { debug_mock: true } : {}), + }), + }); + + if (!res.ok || !res.body) { + const errText = await res.text(); + let msg = `Request failed: ${res.status}`; + try { + const j = JSON.parse(errText) as { detail?: unknown }; + if (typeof j.detail === "string") msg = j.detail; + } catch { + /* ignore */ + } + throw new Error(msg); + } + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let currentEvent = "message"; + let accumulated = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.startsWith("event: ")) { + currentEvent = line.slice(7).trim(); + } else if (line.startsWith("data: ")) { + const raw = line.slice(6); + if (!raw.trim()) continue; + try { + const parsed: unknown = JSON.parse(raw); + if (currentEvent === "token") { + const o = parsed as { token?: string; text?: string }; + const piece = o.token ?? o.text ?? ""; + if (piece) { + accumulated += piece; + setStreamingTokens(accumulated); + } + } else if (currentEvent === "plan") { + const p = parsed as { mode?: string; strength?: number }; + if (p.mode === "research" && typeof p.strength === "number") { + setStatusLine( + `Research · ${researchIntensityLabel(p.strength)} intensity`, + ); + } else if (p.mode === "chat") { + setStatusLine("Chat mode"); + } + } else if (currentEvent === "step") { + const s = parsed as { + step_id?: number; + step_type?: string; + description?: string; + }; + const label = + s.description != null && String(s.description).trim() + ? `Step ${s.step_id ?? "?"}: ${s.description}` + : `Step ${s.step_id ?? "?"} (${s.step_type ?? "?"})`; + setStatusLine(label); + } else if (currentEvent === "done") { + const d = parsed as { message_id?: string; token_count?: number | null }; + const doneMsg: MessageResponse = { + id: d.message_id || `assistant-${Date.now()}`, + role: "assistant", + content: accumulated, + token_count: d.token_count ?? null, + created_at: new Date().toISOString(), + }; + setMessages((prev) => [...prev, doneMsg]); + setStreamingTokens(""); + accumulated = ""; + } else if (currentEvent === "error") { + const err = parsed as { message?: string }; + setError(err.message || "Response error"); + } + } catch { + // Ignore non-JSON data lines + } + } + } + } + } catch (e: any) { + setError(e?.message || "Connection failed"); + } finally { + setStreaming(false); + setStreamingTokens(""); + setStatusLine(null); + overrides?.onComplete?.(); + } + }, + [ + input, + streaming, + threadId, + executionMode, + chatVariant, + researchStrength, + debugMockResearch, + followUpChatOnly, + selectedModelId, + llmCatalog?.models, + accessReady, + llmCatalogLoading, + ], + ); + + useEffect(() => { + if (!launchWith || !accessReady) return; + if (llmCatalogLoading) return; + const list = llmCatalog?.models ?? []; + const bootKey = `${threadId}:${launchWith.nonce}`; + if (list.length === 0) { + if (!launchedBootstrapKeys.has(bootKey)) { + launchedBootstrapKeys.add(bootKey); + onLaunchConsumed?.(); + } + return; + } + if (launchedBootstrapKeys.has(bootKey)) return; + launchedBootstrapKeys.add(bootKey); + const bootMode = followUpChatOnly ? "chat" : launchWith.execution_mode; + setExecutionMode(bootMode); + setExtendedOn(launchWith.chat_variant === "extended"); + setResearchStrength(clampResearchIntensityTier(launchWith.research_strength)); + const fromLaunch = launchWith.model_id?.trim(); + const mid = + fromLaunch && list.some((m) => m.model_id === fromLaunch) + ? fromLaunch + : list.some((m) => m.model_id === selectedModelId) + ? selectedModelId + : list[0]!.model_id; + void runSend({ + content: launchWith.message, + execution_mode: bootMode, + chat_variant: launchWith.chat_variant, + research_strength: launchWith.research_strength, + model_id: mid, + onComplete: () => onLaunchConsumed?.(), + }); + }, [ + launchWith, + threadId, + runSend, + onLaunchConsumed, + accessReady, + followUpChatOnly, + selectedModelId, + llmCatalogLoading, + llmCatalog?.models, + ]); + + const sendMessage = useCallback(() => { + void runSend(); + }, [runSend]); + + const shellClass = + placement === "overlay" + ? "fixed right-0 top-14 z-[60] flex min-h-0 w-[min(100vw,380px)] flex-col overflow-hidden border-l border-neutral-200/80 bg-white" + : placement === "main" + ? "flex min-h-0 h-full max-h-full w-full min-w-0 flex-1 flex-col overflow-hidden bg-[var(--rpt-bg)]" + : placement === "split" + ? "relative flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden bg-white" + : "flex h-full min-h-0 w-[min(100vw,380px)] shrink-0 flex-col overflow-hidden border-l border-neutral-200/80 bg-white"; + + const shellStyle = + placement === "overlay" + ? { + height: "calc(100dvh - 3.5rem)", + maxHeight: "calc(100dvh - 3.5rem)", + } + : undefined; + + return ( + + {placement === "split" ? ( +
+
+
+

Report chat

+

Includes this report

+ {statusLine ? ( +

{statusLine}

+ ) : null} +
+
+ ) : placement === "overlay" || placement === "dock" ? ( +
+
+

+ {standalone ? "Chat" : "Report chat"} +

+ {!standalone ? ( +

Includes this report

+ ) : null} + {statusLine ? ( +

{statusLine}

+ ) : null} +
+ +
+ ) : null} + +
+ {placement === "main" && statusLine ? ( +
+ {statusLine} +
+ ) : null} + {!accessReady && threadId ? ( +
+ +

Loading conversation…

+
+ ) : messages.length === 0 && !streaming ? ( +
+

+ {standalone + ? "Continue the conversation below." + : "Ask a question about this report."} +

+
+ ) : null} + {messages.map((msg) => ( + + ))} + {streaming && streamingTokens ? ( + + ) : null} + {streaming && !streamingTokens ? ( +
+ + Thinking… +
+ ) : null} + {error ? ( +

+ {error} +

+ ) : null} +
+
+ +
+
+
+ {!followUpChatOnly ? ( +
+ + +
+ ) : null} + + {showModelPicker ? ( +
+ Model + +
+ ) : null} + + {followUpChatOnly || executionMode === "chat" ? ( +
+ Extended + +
+ ) : ( +
+ Intensity +
+ {RESEARCH_INTENSITY_OPTIONS.map(({ tier, label }) => ( + + ))} +
+ {showDebugMockResearchControls(session?.user?.email) ? ( + + ) : null} +
+ )} +
+ +
+