diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6dab358 --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# Teacher AI Explainer — Environment Configuration +# Copy this file to .env and fill in your values. + +# LLM provider (e.g. gemini, openai, anthropic, ollama) +LLM_PROVIDER=gemini + +# Model name (e.g. gemini-2.5-pro, gpt-4o, claude-sonnet-4-20250514, llama3.1) +MODEL_NAME=gemini-2.5-pro + +# API key for the provider (not needed for local Ollama) +LLM_API_KEY= + +# Custom API base URL (optional) +# For Ollama Cloud: https://api.ollama.c loud +# For OpenAI-compatible local: http://localhost:11434/v1 +LLM_URL= + +# Request timeout in seconds (default: 120) +LLM_TIMEOUT=120 + +# Server port (default: 8000) +PORT=8000 diff --git a/.gcloudignore b/.gcloudignore deleted file mode 100644 index cbed269..0000000 --- a/.gcloudignore +++ /dev/null @@ -1,10 +0,0 @@ -.git -.gitignore -venv/ -env/ -node_modules/ -__pycache__/ -*.pyc -.next/ -dist/ -build/ \ No newline at end of file diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000..5437f87 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,49 @@ +# Security audit — runs weekly (Sunday) and on PRs: dependency scan, SAST, secrets detection +name: Security Audit + +on: + schedule: + - cron: "0 0 * * 0" + pull_request: + branches: [main] + +jobs: + audit: + name: Security Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: requirements.txt + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: whiteboard-dashboard/package-lock.json + + - name: Install Python audit tools + run: | + python -m pip install --upgrade pip + pip install pip-audit bandit trufflehog + + - name: pip-audit (Python deps) + run: pip-audit -r requirements.txt + continue-on-error: true + + - name: npm audit (Node deps) + run: npm audit + working-directory: whiteboard-dashboard + continue-on-error: true + + - name: bandit (Python SAST) + run: bandit -r . -x tests/ + continue-on-error: true + + - name: truffleHog (Secrets) + run: trufflehog filesystem . --fail + continue-on-error: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6fbaa18 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +# CI pipeline — runs on push/PR to main: lint, type check, test, build +name: CI + +on: + push: + branches: [main, chore/typescriptmigration] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + backend: + name: Backend (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12"] + defaults: + run: + working-directory: . + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: requirements.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff pytest + pip install -r requirements.txt + + - name: Lint (ruff) + run: ruff check . + + - name: Test (pytest) + run: pytest tests/ -v + + frontend: + name: Frontend + runs-on: ubuntu-latest + defaults: + run: + working-directory: whiteboard-dashboard + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: whiteboard-dashboard/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint (eslint) + run: npm run lint + + - name: Type check (tsc) + run: npx tsc --noEmit + + - name: Test (vitest) + run: npx vitest run + + - name: Build + run: npm run build diff --git a/.gitignore b/.gitignore index 8e631ba..599a4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts -/__pycache__ +__pycache__/ +*.pyc \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..bb78853 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +teacher-ai-explainer diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6b845bf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,59 @@ +# Teacher AI Explainer — Agent Guide + +## Project structure + +Two independent sub-projects, **not** a monorepo: + +| Layer | Location | Entrypoint | Tech | +|-------|----------|------------|------| +| Backend | `/` (root) | `main.py` (legacy), `teacher_ai.py` (new) | FastAPI + WebSocket + LiteLLM | +| Backend package | `teacher_ai/` | `standalone.py` | LiteLLM, FastAPI | +| Frontend | `whiteboard-dashboard/` | `src/main.tsx` | React 19 + Vite + ReactFlow + d3-force | + +## Commands + +### Frontend (`whiteboard-dashboard/`) +```sh +npm run dev # Vite dev server (localhost:5173) +npm run build # Vite build → dist/ +npm run lint # ESLint v9 (flat config) +npm run typecheck # tsc --noEmit +npm run preview # Vite preview +``` + +### Backend (root) +```sh +python teacher_ai.py # standalone mode (default, LiteLLM + WebSocket) +python teacher_ai.py --mode mcp # MCP mode (not yet implemented) +uvicorn main:app --reload # legacy dev server (Vertex AI) +pytest tests/ -v # run backend tests +ruff check . # lint backend +``` + +### CI/CD +GitHub Actions workflows in `.github/workflows/`: +- `ci.yml` — runs on push/PR to main: lint (ruff + eslint), type check (tsc), test (pytest + vitest), build (vite) +- `audit.yml` — weekly security audit (pip-audit, npm audit, bandit, truffleHog) + +Run CI locally with `act` before pushing: +```sh +act -j backend --pull=false # Python lint + test +act -j frontend --pull=false # frontend lint + typecheck + test + build +``` + +## Critical gotchas + +- **WebSocket URL is hardcoded** in `App.tsx:96` to a Cloud Run deployment. For local dev, change to `ws://localhost:8000/ws/reason`. +- **No `.env` files.** Backend requires `GOOGLE_CLOUD_PROJECT` + `GOOGLE_CLOUD_LOCATION` for legacy `main.py`, or `LLM_PROVIDER` + `LLM_API_KEY` for `teacher_ai.py`. +- **Tailwind v4** is in devDeps but has no config and is not imported in CSS — likely unused. +- **`.gitignore`** has Next.js entries (`/.next/`, `/out/`) — stale from template. +- **Root `package.json` is stale** — only exists for hoisted deps. Real frontend is in `whiteboard-dashboard/`. + +## Architecture + +- **Dual-mode entrypoint:** `teacher_ai.py --mode standalone|mcp` (default: standalone). +- **Standalone mode** (`teacher_ai/standalone.py`): FastAPI + WebSocket + LiteLLM. Calls any LLM provider via `LLM_PROVIDER` env var (e.g. `gemini/gemini-2.5-pro`, `openai/gpt-4o`, `anthropic/claude-sonnet-4-20250514`, `ollama/llama3.1`). LiteLLM telemetry disabled by default. +- **Legacy mode** (`main.py`): FastAPI + Vertex AI Gemini. Requires GCP project/location env vars. +- Frontend: `FlowBoard` component manages ReactFlow graph + d3-force layout + WebSocket client + conversation history sidebar. Custom node type `mathNode` renders KaTeX via `react-markdown` + `remark-math` + `rehype-katex`. +- Node types: `Given`, `Objective`, `Principle`, `Derivation`, `Self-Correction`, `Alternative`, `Final Answer`. +- Probe feature: clicking `?` on a node sends `{"type":"probe","parent_id":"...","content":"..."}` to get an alternative explanation. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f490d4c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -# 1. Use a lightweight Python image -FROM python:3.11-slim - -# 2. Prevent Python from buffering stdout/stderr (better logs in GCP) -ENV PYTHONUNBUFFERED=1 - -# 3. Set the working directory -WORKDIR /app - -# 4. Copy requirements and install -# Note: Make sure 'google-cloud-aiplatform' is in your requirements.txt! -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# 5. Copy the rest of your code -COPY . . - -# 6. Run the app with Uvicorn -# Cloud Run expects the app to listen on port 8080 by default -# Use "exec" so the app receives signals correctly, and bind to 0.0.0.0 -CMD exec uvicorn main:app --host 0.0.0.0 --port ${PORT} \ No newline at end of file diff --git a/main.py b/main.py index ceb0ab3..c0167f2 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,18 @@ +# FastAPI server that connects Gemini (Vertex AI) to a real-time whiteboard via WebSocket. +# Receives STEM questions, streams reasoning nodes back as the model generates them. + import asyncio import json +import os + from fastapi import FastAPI, WebSocket +from fastapi.middleware.cors import CORSMiddleware from google import genai from google.genai import types -from fastapi.middleware.cors import CORSMiddleware -import os app = FastAPI() + +# Allow the Vite dev server and Firebase-hosted frontends to connect origins = [ "http://localhost:5173", "https://cs598-project-492002.web.app", @@ -14,20 +20,22 @@ ] app.add_middleware( CORSMiddleware, - allow_origins=origins, # For a prototype, "*" is fine. For production, use your Vercel/Firebase URL. + allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) -project_id=os.environ.get("GOOGLE_CLOUD_PROJECT") -location_id=os.environ.get("GOOGLE_CLOUD_LOCATION","us-central1") +# Authenticate with Vertex AI using GCP project/location env vars +project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") +location_id = os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1") if project_id and location_id: - client = genai.Client(vertexai=True,project=project_id,location=location_id) + client = genai.Client(vertexai=True, project=project_id, location=location_id) else: - raise ValueError + raise ValueError("GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION must be set") +# Declare the function-calling tool that Gemini uses to add nodes to the graph node_tool = types.Tool( function_declarations=[ types.FunctionDeclaration( @@ -69,7 +77,8 @@ async def websocket_endpoint(websocket: WebSocket): while True: raw = await websocket.receive_text() - # --- Route: probe vs regular query --- + # Determine if this is a probe request (asking for alternative explanation) + # or a regular question try: msg = json.loads(raw) is_probe = msg.get("type") == "probe" @@ -90,7 +99,7 @@ async def websocket_endpoint(websocket: WebSocket): else: student_query = raw - # --- Fresh chat session per query/probe --- + # Start a fresh Gemini chat session for each query chat = client.chats.create( model='gemini-2.5-pro', config=types.GenerateContentConfig( @@ -102,6 +111,8 @@ async def websocket_endpoint(websocket: WebSocket): response = chat.send_message(student_query) + # Loop through Gemini's function calls, sending each node to the frontend + # with a 1.2s delay so the graph builds incrementally while True: found_tool_call = False @@ -110,8 +121,7 @@ async def websocket_endpoint(websocket: WebSocket): found_tool_call = True node_data = dict(part.function_call.args) - # For probes, force the correct parent_id and type - # in case the model ignores the instruction + # Override probe responses to ensure correct structure if is_probe: node_data["node_type"] = "Alternative" node_data["parent_id"] = parent_id @@ -120,10 +130,14 @@ async def websocket_endpoint(websocket: WebSocket): await websocket.send_json(node_data) print(f"Node sent: {node_data.get('label')} [{node_data.get('node_type')}]") + # Tell Gemini the node was rendered so it can continue response = chat.send_message( types.Part.from_function_response( name="add_reasoning_node", - response={"status": "success", "message": "Node rendered on whiteboard"} + response={ + "status": "success", + "message": "Node rendered on whiteboard" + } ) ) diff --git a/openspec/changes/teacher-ai-v2/.openspec.yaml b/openspec/changes/teacher-ai-v2/.openspec.yaml new file mode 100644 index 0000000..8e7013b --- /dev/null +++ b/openspec/changes/teacher-ai-v2/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-27 diff --git a/openspec/changes/teacher-ai-v2/design.md b/openspec/changes/teacher-ai-v2/design.md new file mode 100644 index 0000000..9ac5f7a --- /dev/null +++ b/openspec/changes/teacher-ai-v2/design.md @@ -0,0 +1,248 @@ +## Context + +The current prototype is a FastAPI backend with a hardcoded Gemini Vertex AI client, serving a React frontend deployed on Firebase. The backend owns the LLM call loop, the API key, and the provider choice. To make this a local-first tool, we re-architect around the MCP protocol: the backend becomes an MCP server that exposes graph-building tools, and the user's LLM client (Claude Desktop, opencode, etc.) drives the conversation. The browser becomes a peripheral display for the reasoning graph. + +To keep the existing UX working while adding MCP support, the server runs in one of two modes: `standalone` (current Gemini + WebSocket behavior) or `mcp` (MCP protocol-driven, browser as display). + +## Goals / Non-Goals + +**Goals:** +- Dual-mode entrypoint: standalone (Gemini-driven) and MCP (LLM-client-driven) modes from a single `teacher_ai.py` command +- MCP server with `add_reasoning_node`, `probe_node`, and session management tools +- WebSocket bridge for real-time graph updates in the browser +- SQLite-backed session persistence with auto-save, browse, resume +- Settings persistence via JSON config file +- TypeScript migration of the frontend +- Single-command local startup (server + browser) +- Zero cloud dependencies, zero server-side API keys +- BYOK in standalone mode via LiteLLM (OpenAI, Anthropic, Gemini, Ollama, and 100+ providers) + +**Non-Goals:** +- Multi-user support (single-user local tool) +- Svelte migration (deferred) +- Authentication or authorization + +## Decisions + +### Decision: Dual-mode entrypoint with shared infrastructure +A single `teacher_ai.py` entrypoint accepts `--mode standalone|mcp` (default: standalone). Both modes share: WebSocket server for browser graph updates, SQLite session persistence, JSON settings store, static file serving, and browser auto-launch. The standalone mode wraps the current `main.py` logic; MCP mode exposes graph-building tools via the MCP SDK. + +**Alternatives considered:** +- Two separate entrypoints: More files, more confusion. Single entrypoint with `--mode` is cleaner. +- Remove standalone mode entirely: Breaks existing users who like the current UX. Keep both. + +### Decision: LiteLLM for provider abstraction in standalone mode +Use `litellm` to support any LLM provider in standalone mode. The user sets `LLM_PROVIDER` (e.g. `gemini/gemini-2.5-pro`, `openai/gpt-4o`, `anthropic/claude-sonnet-4-20250514`, `ollama/llama3.1`) and optionally `LLM_API_KEY`. LiteLLM normalizes tool calling across all providers into a single `completion()` call. + +**Alternatives considered:** +- Custom provider wrappers: More code to maintain. LiteLLM already handles 100+ providers. +- Hardcode Vertex AI only: Locks users to GCP. BYOK is a core requirement. + +### Decision: MCP server in Python with `mcp` SDK +Use the official Python `mcp` library. It supports both stdio transport (for CLI clients like opencode) and SSE transport (for Claude Desktop). The server runs as a standalone process. + +**Alternatives considered:** +- Node.js MCP server: Would unify the stack but requires rewriting the backend. Python keeps the existing logic and the `mcp` SDK is mature. +- Raw stdio protocol: Too low-level. The SDK handles JSON-RPC framing, tool registration, and error handling. + +### Decision: WebSocket server alongside MCP server +Run a separate WebSocket server (using `websockets` library) on a different port alongside the MCP server. The MCP server's tool handlers broadcast to WebSocket clients. + +**Alternatives considered:** +- Embed WebSocket in MCP server: The MCP SDK doesn't natively support WebSocket. Running them side-by-side is simpler. +- SSE from MCP server to browser: Browsers can consume SSE, but WebSocket is bidirectional (needed for future features like session switching from the browser). + +### Decision: SQLite via stdlib `sqlite3` +Use Python's built-in `sqlite3` module. No ORM, no migration framework. The schema is simple (3 tables: sessions, nodes, edges). + +**Alternatives considered:** +- SQLAlchemy: Overkill for 3 tables. Adds a dependency. +- JSON file storage: Would work but doesn't scale to many sessions. SQLite is simpler for querying (list sessions sorted by date, count nodes per session, etc.). + +### Decision: Clipboard via `pyperclip` +Use `pyperclip` for cross-platform clipboard access when the probe tool is called. Falls back to printing the prompt to stdout if clipboard is unavailable. + +**Alternatives considered:** +- Platform-specific commands (`xclip`, `pbcopy`, `clip`): Works but requires platform detection. `pyperclip` handles this. + +### Decision: Frontend build automation via subprocess +The Python server checks if `whiteboard-dashboard/dist/` exists on startup. If not, it runs `npm install && npm run build` via subprocess. This keeps the setup friction-free. + +**Alternatives considered:** +- Require manual build: More steps for the user. Bad for "download and run." +- Bundle frontend in Python package: Complex. Subprocess is simpler. + +### Decision: TypeScript migration with minimal types +Add a `tsconfig.json`, rename `.jsx` to `.tsx`, and define interfaces for the core data types (NodeData, ProbeMessage, ReasoningNode). Use `@xyflow/react`'s built-in types. No strict mode initially — pragmatic over purity. + +**Alternatives considered:** +- Full strict TypeScript: Would require more refactoring of the d3-force mutation pattern. Start lenient, tighten later. +- Keep JS: Loses the benefits of type safety for the WebSocket protocol. Not worth it. + +### Decision: Session auto-restore on startup +The server stores the last active session ID in `settings.json`. On startup, it loads that session and sends the graph state to the browser on WebSocket connect. + +**Alternatives considered:** +- Always start fresh: Users lose their last session. Bad UX. +- Show session picker on startup: More complex. Auto-restore with a sidebar to switch is simpler. + +## Architecture + +``` +teacher_ai.py (--mode standalone|mcp) + ├── teacher_ai/standalone.py (LiteLLM + WebSocket loop) + ├── teacher_ai/mcp_server.py (MCP tools + WebSocket broadcast) + ├── teacher_ai/session_manager.py (SQLite) + ├── teacher_ai/settings.py (JSON store) + └── teacher_ai/websocket_server.py (shared WS broadcast) +``` + +### Data Flow + +**Standalone mode:** +1. `python teacher_ai.py` (default) +2. Server reads `LLM_PROVIDER` and `LLM_API_KEY` env vars +3. Server starts WebSocket server + static file serving +4. Browser opens, connects WebSocket +5. User types question in browser +6. Server calls LiteLLM `completion()` with the chosen provider, streams nodes back via WebSocket +7. Probe button sends to LiteLLM for alternative explanation + +**MCP mode:** +1. `python teacher_ai.py --mode mcp` +2. Server starts MCP server + WebSocket server + static file serving +3. Browser opens, connects WebSocket, receives last session state +4. User connects LLM client to MCP server +5. LLM calls `add_reasoning_node` → server broadcasts to browser + saves to SQLite +6. Probe button copies prompt to clipboard + +### File Layout + +``` +teacher_ai.py # Entrypoint (argparse, shared startup) +teacher_ai/ + __init__.py + standalone.py # Standalone mode handler (LiteLLM + WebSocket) + mcp_server.py # MCP mode handler + session_manager.py # SQLite persistence + settings.py # JSON settings store + websocket_server.py # Shared WS broadcast server +main.py # Kept for backward compat +``` + +### SQLite Schema + +```sql +CREATE TABLE sessions ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT 'New Session', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE nodes ( + id TEXT NOT NULL, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + node_type TEXT NOT NULL, + label TEXT NOT NULL, + content TEXT NOT NULL, + parent_id TEXT, + position_x REAL, + position_y REAL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (id, session_id) +); + +CREATE TABLE edges ( + id TEXT NOT NULL, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + source TEXT NOT NULL, + target TEXT NOT NULL, + PRIMARY KEY (id, session_id) +); +``` + +### MCP Tool Definitions + +``` +add_reasoning_node(id, label, content, node_type, parent_id?) + → broadcasts to WebSocket, saves to SQLite, returns {status: "success"} + +probe_node(parent_id, content) + → copies prompt to clipboard, returns prompt text + +list_sessions() + → returns [{id, title, created_at, updated_at, node_count}] + +load_session(id) + → sets active session, broadcasts full graph to WebSocket + +delete_session(id) + → removes session + nodes + edges from SQLite + +rename_session(id, title) + → updates session title in SQLite +``` + +### Env Var Configuration + +``` +LLM_PROVIDER=gemini/gemini-2.5-pro # any litellm model string (standalone mode) +LLM_API_KEY=... # API key for the provider (optional for Ollama) +OLLAMA_BASE_URL=http://localhost:11434 # optional, default +``` + +## Test Strategy + +**Isolation:** In-memory SQLite for all tests. Mock MCP transport layer. `asyncio` test helpers for WebSocket. Tests use `pytest-asyncio` for async test functions. + +**Pipeline stages (in order):** +1. **Lint** — `ruff` (Python), `eslint` (frontend) +2. **Type check** — `mypy` (Python), `tsc --noEmit` (frontend) +3. **Unit tests** — `pytest` (Python), `vitest` (frontend) +4. **Integration tests** — MCP → WebSocket → browser handshake +5. **Adversarial tests** — flood, injection, XSS, RCE paths +6. **Edge case tests** — large graphs, concurrent, corrupt DB +7. **Build** — `npm run build`, verify server starts +8. **Audit** — dependency scan, SAST, secrets detection (weekly scheduled) + +**Rate limiter:** Token-bucket algorithm, 30 tokens/sec per connection, max burst 60. Returns `{"error": "rate_limit", "retry_after": 0.033}` on throttle. Implemented as an `asyncio` middleware on the WebSocket server. + +**Input sanitization:** +- Content truncated at 100KB, labels at 200 chars, IDs at 100 chars +- HTML tags stripped via regex before WebSocket broadcast +- Original content (with tags) preserved in SQLite +- All database queries use parameterized statements exclusively +- Subprocess paths validated against a whitelist (`["npm", "node", "npx"]`) + +**Test data generators:** +- `generate_tree(depth, breadth)` — creates a balanced tree of nodes for large-graph tests +- `generate_latex(size)` — generates LaTeX math of specified size +- `generate_injection_payloads()` — returns a list of SQL/XSS/prompt injection strings + +**Coverage targets:** +- Python: 90%+ line coverage (pytest-cov) +- Frontend: 80%+ line coverage (vitest --coverage) +- Coverage enforced on CI (non-blocking, reported as comment) + +## Security Audit + +**Weekly scheduled workflow** (Sunday 00:00 UTC): +- `pip-audit` — Python dependency vulnerabilities +- `npm audit` — Node dependency vulnerabilities +- `bandit` — Python SAST +- `truffleHog` — Secrets scanning +- Results posted as a GitHub issue if findings exist + +**PR audit gate:** +- `pip-audit` runs on every PR (non-blocking, warning only) +- `truffleHog` runs on every PR (blocking if secrets found) + +## Risks / Trade-offs + +- **[Risk] MCP SDK maturity**: The Python MCP SDK is relatively new. If it has bugs, fall back to raw stdio JSON-RPC. **Mitigation**: Pin a known-good version in requirements.txt. +- **[Risk] WebSocket port conflict**: If the default port is in use, the server should pick a random port and print it. **Mitigation**: Port fallback logic in the launcher. +- **[Risk] Clipboard fails in headless environments**: `pyperclip` may fail on systems without a clipboard (WSL, SSH). **Mitigation**: Fall back to printing the prompt to stdout. +- **[Risk] LiteLLM dependency size**: LiteLLM pulls in many sub-dependencies (~50 MB). **Mitigation**: Acceptable for a local tool; only installed in standalone mode path. +- **[Trade-off] Two ports**: MCP server and WebSocket server run on different ports. Slightly more complex than a single-port solution, but each protocol gets its own lifecycle. +- **[Trade-off] LiteLLM vs custom wrappers**: LiteLLM adds ~50 MB but saves writing and maintaining 4+ provider adapters. Worth it for BYOK simplicity. diff --git a/openspec/changes/teacher-ai-v2/proposal.md b/openspec/changes/teacher-ai-v2/proposal.md new file mode 100644 index 0000000..789440e --- /dev/null +++ b/openspec/changes/teacher-ai-v2/proposal.md @@ -0,0 +1,42 @@ +## Why + +The current prototype is tied to Google Cloud (Vertex AI + Gemini + Cloud Run + Firebase), requires a deployed backend, and has no path for users to bring their own LLM API key. To make this a real tool anyone can download and run locally, we need to strip all cloud dependencies, decouple from a single LLM provider, and re-architect around the MCP protocol so the graph becomes a peripheral reasoning display for any MCP-compatible LLM client. Sessions and settings must persist across restarts like OpenWebUI. + +## What Changes + +- **CHANGE**: Backend refactored from single `main.py` to dual-mode `teacher_ai.py` entrypoint with `teacher_ai/` package — standalone mode (LiteLLM + WebSocket, BYOK) and MCP mode (protocol-driven, browser as display) +- **BREAKING**: Remove all cloud infrastructure — Firebase hosting, Cloud Run, Dockerfile, GCP configs, Vertex AI SDK +- **BREAKING**: Frontend JS → TypeScript migration (React retained, Svelte deferred) +- **NEW**: LiteLLM for provider abstraction in standalone mode — supports OpenAI, Anthropic, Gemini, Ollama, and 100+ providers via `LLM_PROVIDER` env var +- **NEW**: WebSocket bridge from MCP server to browser for real-time graph updates +- **NEW**: Probe button copies pre-formatted prompt to clipboard instead of sending WebSocket message (MCP mode) +- **NEW**: Persistent session storage (SQLite) — auto-save graphs, browse history, resume sessions +- **NEW**: Settings store (JSON) — user preferences survive restarts +- **NEW**: Config file / env var support for local setup +- **NEW**: CI/CD pipeline with GitHub Actions — lint, type check, test suites, build, and security audit +- **FIX**: Add missing runtime dependencies (`react-markdown`, `remark-math`, `rehype-katex`) to `package.json` +- **REMOVE**: ~295 unused Python dependencies from `requirements.txt` + +## Capabilities + +### New Capabilities +- `dual-mode`: Single `teacher_ai.py` entrypoint with `--mode standalone|mcp` (default: standalone) +- `byok`: Bring-your-own-key via `LLM_PROVIDER` and `LLM_API_KEY` env vars — works with OpenAI, Anthropic, Gemini, Ollama, and 100+ providers via LiteLLM +- `mcp-server`: MCP server exposing `add_reasoning_node`, `probe_node`, and session management tools, with WebSocket bridge to browser +- `local-setup`: Download-and-run experience — single command starts server + opens browser +- `session-persistence`: SQLite-backed session storage with auto-save, history browsing, resume, and settings persistence +- `ci-cd`: GitHub Actions pipeline with lint, type check, test suites (functional, integration, adversarial, edge case), build, and security audit + +### Modified Capabilities +- (none — no existing specs to modify) + +## Impact + +- `main.py` → kept for backward compat; new `teacher_ai.py` is the primary entrypoint +- `teacher_ai/` package added with `standalone.py`, `mcp_server.py`, `session_manager.py`, `settings.py`, `websocket_server.py` +- `requirements.txt` → stripped of Vertex AI, GCP, and unused ML packages; add `litellm`, `mcp`, `websockets`, `pyperclip` +- `Dockerfile`, `.gcloudignore`, `firebase.json`, `.firebaserc` → removed +- `whiteboard-dashboard/src/*.jsx` → renamed to `.tsx`, types added +- `whiteboard-dashboard/package.json` → add missing deps, add `typescript`, `tsconfig.json` +- `System_Prompt.md` → embedded in MCP tool descriptions +- New files: `.env.example`, `tsconfig.json`, `src/types.ts`, `src/SessionSidebar.tsx`, `src/SettingsPanel.tsx` diff --git a/openspec/changes/teacher-ai-v2/specs/byok/spec.md b/openspec/changes/teacher-ai-v2/specs/byok/spec.md new file mode 100644 index 0000000..dffee7b --- /dev/null +++ b/openspec/changes/teacher-ai-v2/specs/byok/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Server requires no API keys +The MCP server SHALL NOT require any API keys or cloud credentials. All LLM API keys are managed by the user's LLM client (Claude Desktop, opencode, etc.). + +#### Scenario: Server starts without any API keys +- **WHEN** the user starts the MCP server without any API keys configured +- **THEN** the server starts successfully and is ready for LLM client connections + +### Requirement: Config file for user preferences +The system SHALL read user preferences from `~/.teacher-ai-explainer/settings.json`, including theme, default view, and other UI preferences. + +#### Scenario: Settings file exists +- **WHEN** the application starts and `~/.teacher-ai-explainer/settings.json` exists +- **THEN** the system loads and applies the settings + +#### Scenario: Settings file does not exist +- **WHEN** the application starts and `~/.teacher-ai-explainer/settings.json` does not exist +- **THEN** the system creates it with default values + +### Requirement: Environment variable support +The system SHALL support environment variables for configuration, with env vars overriding config file values. + +#### Scenario: Env var overrides config file +- **WHEN** both `TEACHER_AI_PORT` env var and a port in `settings.json` are set +- **THEN** the env var value takes precedence diff --git a/openspec/changes/teacher-ai-v2/specs/ci-cd/spec.md b/openspec/changes/teacher-ai-v2/specs/ci-cd/spec.md new file mode 100644 index 0000000..677e1aa --- /dev/null +++ b/openspec/changes/teacher-ai-v2/specs/ci-cd/spec.md @@ -0,0 +1,136 @@ +## ADDED Requirements + +### Requirement: CI pipeline runs on every PR and push to main +GitHub Actions SHALL run lint, type check, unit tests, integration tests, adversarial tests, edge case tests, and build on every PR and push to main. The pipeline SHALL use a matrix strategy across Python 3.11 and 3.12. + +#### Scenario: PR opened +- **WHEN** a PR is opened against main +- **THEN** the CI pipeline runs all stages +- **THEN** the PR shows check status for each stage + +#### Scenario: Push to main +- **WHEN** code is pushed to main +- **THEN** the CI pipeline runs all stages +- **THEN** a failure blocks further merges + +### Requirement: Functional tests cover all MCP tools +Tests SHALL verify `add_reasoning_node`, `probe_node`, `list_sessions`, `load_session`, `delete_session`, `rename_session` with valid inputs, missing fields, invalid types, and nonexistent session IDs. + +#### Scenario: add_reasoning_node with valid data +- **WHEN** the test calls `add_reasoning_node` with valid arguments +- **THEN** the node is saved to SQLite +- **THEN** the node is broadcast via WebSocket +- **THEN** the tool returns success + +#### Scenario: add_reasoning_node with missing fields +- **WHEN** the test calls `add_reasoning_node` without required fields +- **THEN** the tool returns an error response + +### Requirement: Integration tests cover MCP to WebSocket to browser handshake +Tests SHALL verify the full data flow: MCP tool call triggers WebSocket broadcast, browser client receives the correct message, session switch propagates full graph state. + +#### Scenario: Node added propagates to WebSocket client +- **WHEN** a test WebSocket client is connected +- **WHEN** `add_reasoning_node` is called via MCP +- **THEN** the WebSocket client receives the node data + +#### Scenario: Session switch propagates full graph +- **WHEN** `load_session` is called via MCP +- **THEN** the WebSocket client receives the full graph state of that session + +### Requirement: Adversarial tests cover security boundaries +Tests SHALL verify: WebSocket flood (rate limiter rejects excess), SQL injection (parameterized queries prevent it), XSS (content sanitized before broadcast), prompt injection (content length limits enforced), RCE (subprocess paths validated). + +#### Scenario: WebSocket flood is rate-limited +- **WHEN** a client sends more than 30 messages per second +- **THEN** the server returns a rate-limit error +- **THEN** the server does not crash + +#### Scenario: SQL injection is prevented +- **WHEN** a tool call includes SQL injection payloads in string fields +- **THEN** the payloads are treated as data, not executed +- **THEN** the database is not corrupted + +#### Scenario: XSS is prevented +- **WHEN** a tool call includes HTML/script tags in content +- **THEN** the content is sanitized before WebSocket broadcast +- **THEN** the browser renders the content as text, not HTML + +#### Scenario: RCE via subprocess is prevented +- **WHEN** the server runs a subprocess (npm build) +- **THEN** the command path is validated against a whitelist +- **THEN** user-controlled input is not passed to the shell + +### Requirement: Edge case tests cover extreme conditions +Tests SHALL verify: 1000+ node graphs, 100-level tree depth, huge LaTeX (100KB+), 50 concurrent WebSocket connections, port conflicts, empty/corrupt SQLite DB, missing clipboard. + +#### Scenario: Large graph with 1000 nodes +- **WHEN** 1000 nodes are added to a session +- **THEN** all nodes are saved to SQLite +- **THEN** the session loads correctly +- **THEN** the WebSocket broadcasts all nodes + +#### Scenario: Corrupt SQLite database +- **WHEN** the SQLite database file is corrupt +- **THEN** the server starts with a fresh database +- **THEN** an error is logged about the corruption + +### Requirement: Rate limiter on WebSocket server +The WebSocket server SHALL implement a token-bucket rate limiter with a maximum of 30 messages per second per connection and a maximum burst of 60 messages. + +#### Scenario: Rate limiter allows normal traffic +- **WHEN** a client sends 25 messages in one second +- **THEN** all messages are processed normally + +#### Scenario: Rate limiter blocks excess traffic +- **WHEN** a client sends 40 messages in one second +- **THEN** messages beyond the limit are rejected with a rate-limit error + +### Requirement: Input sanitization +All string inputs SHALL be length-limited: content to 100KB, label to 200 characters, id to 100 characters. Content SHALL be sanitized to strip HTML tags before WebSocket broadcast. All database queries SHALL use parameterized statements. + +#### Scenario: Content exceeds length limit +- **WHEN** a tool call includes content exceeding 100KB +- **THEN** the content is truncated to 100KB +- **THEN** the truncated content is saved and broadcast + +#### Scenario: HTML tags in content +- **WHEN** a tool call includes HTML tags in content +- **THEN** the tags are stripped before WebSocket broadcast +- **THEN** the original content (with tags) is saved to SQLite + +## ADDED Requirements (Audit) + +### Requirement: Security audit pipeline runs weekly +A scheduled GitHub Actions workflow SHALL run weekly (Sunday) to audit dependencies, scan for secrets, and run SAST analysis. + +#### Scenario: Scheduled audit runs +- **WHEN** the scheduled workflow triggers +- **THEN** dependency scanning runs (pip-audit, npm audit) +- **THEN** SAST analysis runs (bandit for Python) +- **THEN** secrets scanning runs (truffleHog or Gitleaks) +- **THEN** results are reported to the security dashboard + +### Requirement: Dependency scanning +The audit pipeline SHALL scan Python dependencies with `pip-audit` and Node dependencies with `npm audit`. Failures SHALL be reported but not block the pipeline. + +#### Scenario: Vulnerable dependency found +- **WHEN** a dependency has a known vulnerability +- **THEN** the audit report includes the vulnerability details +- **THEN** a GitHub issue is created for the vulnerability + +### Requirement: SAST analysis +The audit pipeline SHALL run `bandit` on Python code and `eslint-plugin-security` on frontend code to detect common security anti-patterns. + +#### Scenario: SAST finds a security issue +- **WHEN** bandit or eslint-plugin-security detects a security issue +- **THEN** the issue is reported with file, line, and severity +- **THEN** the pipeline continues (non-blocking) + +### Requirement: Secrets detection +The audit pipeline SHALL scan the repository for accidentally committed secrets (API keys, tokens, passwords) using `truffleHog` or `Gitleaks`. + +#### Scenario: Secret detected in commit +- **WHEN** a secret is detected in the repository +- **THEN** the audit report includes the file and secret type +- **THEN** the pipeline fails diff --git a/openspec/changes/teacher-ai-v2/specs/local-setup/spec.md b/openspec/changes/teacher-ai-v2/specs/local-setup/spec.md new file mode 100644 index 0000000..06ecf9c --- /dev/null +++ b/openspec/changes/teacher-ai-v2/specs/local-setup/spec.md @@ -0,0 +1,32 @@ +## ADDED Requirements + +### Requirement: Single command starts the application +The system SHALL provide a single command (`python teacher_ai_mcp.py` or equivalent) that starts the MCP server, launches the browser, and is ready for LLM client connections. + +#### Scenario: User runs the start command +- **WHEN** the user runs `python teacher_ai_mcp.py` +- **THEN** the MCP server starts on localhost +- **THEN** the browser opens to the graph UI +- **THEN** the server prints the MCP connection URL for the LLM client + +### Requirement: Frontend is served locally +The MCP server SHALL serve the built frontend (React + TS) as static files on an HTTP endpoint, so no separate hosting is needed. + +#### Scenario: Browser loads the frontend +- **WHEN** the user opens `http://localhost:` in a browser +- **THEN** the React application loads and connects to the WebSocket + +### Requirement: Frontend build is automated +The system SHALL provide a build script that compiles the TypeScript frontend to static files before the server starts. + +#### Scenario: First-time setup +- **WHEN** the user runs the application for the first time +- **THEN** the system checks if the frontend is built +- **THEN** if not built, the system runs `npm install && npm run build` automatically + +### Requirement: No cloud dependencies +The system SHALL run entirely on the local machine with no external cloud services, no API keys on the server side, and no internet requirement beyond the user's LLM API calls. + +#### Scenario: Offline-capable graph UI +- **WHEN** the user runs the application without internet +- **THEN** the graph UI loads and functions (only LLM calls require internet) diff --git a/openspec/changes/teacher-ai-v2/specs/mcp-server/spec.md b/openspec/changes/teacher-ai-v2/specs/mcp-server/spec.md new file mode 100644 index 0000000..0296a4d --- /dev/null +++ b/openspec/changes/teacher-ai-v2/specs/mcp-server/spec.md @@ -0,0 +1,55 @@ +## ADDED Requirements + +### Requirement: MCP server exposes add_reasoning_node tool +The MCP server SHALL expose a tool named `add_reasoning_node` that accepts `id`, `label`, `content` (LaTeX), `node_type` (Given, Objective, Principle, Derivation, Self-Correction, Alternative, Final Answer), and optional `parent_id`. The server SHALL broadcast the node to all connected browser clients via WebSocket and auto-save to the active session. + +#### Scenario: LLM calls add_reasoning_node +- **WHEN** the LLM client calls `add_reasoning_node` with valid arguments +- **THEN** the server broadcasts the node to the browser via WebSocket +- **THEN** the server saves the node to the active session in SQLite +- **THEN** the server returns `{"status": "success"}` to the LLM client + +#### Scenario: add_reasoning_node with missing required fields +- **WHEN** the LLM client calls `add_reasoning_node` without `id`, `label`, `content`, or `node_type` +- **THEN** the server returns an error response describing the missing field + +### Requirement: MCP server exposes probe_node tool +The MCP server SHALL expose a tool named `probe_node` that accepts `parent_id` and `content`. The server SHALL copy a pre-formatted prompt to the system clipboard and return the prompt text to the LLM client. + +#### Scenario: LLM calls probe_node +- **WHEN** the LLM client calls `probe_node` with `parent_id` and `content` +- **THEN** the server copies a pre-formatted prompt to the clipboard +- **THEN** the server returns the prompt text to the LLM client + +### Requirement: MCP server exposes session management tools +The MCP server SHALL expose `list_sessions`, `load_session`, `delete_session`, and `rename_session` tools for browsing and managing saved sessions. + +#### Scenario: List all sessions +- **WHEN** the LLM client calls `list_sessions` +- **THEN** the server returns a list of sessions with id, title, created_at, updated_at, and node_count + +#### Scenario: Load a specific session +- **WHEN** the LLM client calls `load_session` with a valid session id +- **THEN** the server loads that session as active and broadcasts the full graph state to the browser + +#### Scenario: Delete a session +- **WHEN** the LLM client calls `delete_session` with a valid session id +- **THEN** the server removes the session and all its nodes/edges from SQLite + +### Requirement: WebSocket bridge broadcasts graph state +The MCP server SHALL run a WebSocket server that browser clients connect to. The server SHALL broadcast node additions, session switches, and full graph state on connect. + +#### Scenario: Browser connects to WebSocket +- **WHEN** a browser client connects to the WebSocket +- **THEN** the server sends the full graph state of the active session + +#### Scenario: Node added during active session +- **WHEN** a node is added via `add_reasoning_node` +- **THEN** the server broadcasts the node data to all connected browser clients + +### Requirement: System prompt embedded in tool descriptions +The MCP server SHALL embed the pedagogical system instructions (Given → Principle → Derivation → Self-Correction → Final Answer methodology) in the tool descriptions so the LLM client can present them to the model. + +#### Scenario: LLM client reads tool descriptions +- **WHEN** the LLM client connects to the MCP server and reads tool descriptions +- **THEN** the descriptions SHALL include the reasoning methodology and node type taxonomy diff --git a/openspec/changes/teacher-ai-v2/specs/session-persistence/spec.md b/openspec/changes/teacher-ai-v2/specs/session-persistence/spec.md new file mode 100644 index 0000000..4e72787 --- /dev/null +++ b/openspec/changes/teacher-ai-v2/specs/session-persistence/spec.md @@ -0,0 +1,60 @@ +## ADDED Requirements + +### Requirement: Sessions auto-save to SQLite +Every node added via `add_reasoning_node` SHALL be automatically saved to a SQLite database at `~/.teacher-ai-explainer/sessions.db`. No manual save action is required. + +#### Scenario: Node added during session +- **WHEN** the LLM client calls `add_reasoning_node` +- **THEN** the node is saved to the SQLite database with session_id, node_id, type, label, content, parent_id, and position + +#### Scenario: Multiple sessions +- **WHEN** the user starts a new session +- **THEN** a new session record is created in SQLite +- **THEN** subsequent nodes are associated with the new session + +### Requirement: Sessions are browsable and resumable +The system SHALL provide a session sidebar in the browser UI listing all saved sessions with titles and timestamps. Users SHALL be able to click a session to restore its full graph state. + +#### Scenario: User browses sessions +- **WHEN** the user opens the application +- **THEN** the session sidebar shows a list of all saved sessions with title, date, and node count + +#### Scenario: User resumes a session +- **WHEN** the user clicks a session in the sidebar +- **THEN** the graph restores all nodes, edges, and positions for that session +- **THEN** the session becomes the active session for new nodes + +### Requirement: Last session auto-restores on startup +When the application starts, it SHALL automatically load the most recently active session. + +#### Scenario: Application restarts +- **WHEN** the user restarts the application +- **THEN** the last active session is loaded +- **THEN** the graph displays the previous state + +### Requirement: Session titles are editable +Users SHALL be able to rename sessions. New sessions SHALL auto-generate a title from the first question text. + +#### Scenario: Auto-generate session title +- **WHEN** a new session is created and the first node is added +- **THEN** the session title is set to the first question text (truncated to 80 chars) + +#### Scenario: Rename session +- **WHEN** the user renames a session via the sidebar +- **THEN** the new name is saved to SQLite + +### Requirement: Settings persist across restarts +User preferences (theme, default view, port, etc.) SHALL be saved to `~/.teacher-ai-explainer/settings.json` and restored on application start. + +#### Scenario: User changes a setting +- **WHEN** the user changes a setting in the UI +- **THEN** the setting is saved to `settings.json` immediately +- **THEN** the setting is applied on next application start + +### Requirement: Sessions can be deleted +Users SHALL be able to delete sessions from the sidebar. Deletion SHALL remove all associated nodes and edges from SQLite. + +#### Scenario: User deletes a session +- **WHEN** the user deletes a session from the sidebar +- **THEN** the session and all its nodes/edges are removed from SQLite +- **THEN** the session is removed from the sidebar diff --git a/openspec/changes/teacher-ai-v2/tasks.md b/openspec/changes/teacher-ai-v2/tasks.md new file mode 100644 index 0000000..218bf51 --- /dev/null +++ b/openspec/changes/teacher-ai-v2/tasks.md @@ -0,0 +1,94 @@ +## 1. Strip Cloud Infrastructure + +- [x] 1.1 Remove `Dockerfile`, `.gcloudignore`, `firebase.json`, `.firebaserc` +- [x] 1.2 Strip `requirements.txt` — remove Vertex AI, GCP, and unused ML packages (keep FastAPI, uvicorn, litellm, mcp, websockets, pyperclip) +- [ ] 1.3 Create `teacher_ai.py` dual-mode entrypoint + `teacher_ai/` package (keep `main.py` for backward compat) + +## 2. TypeScript Migration + +- [x] 2.1 Add `tsconfig.json` to `whiteboard-dashboard/` +- [x] 2.2 Add missing runtime deps to `package.json`: `react-markdown`, `remark-math`, `rehype-katex` +- [x] 2.3 Add dev deps: `typescript`, `@types/d3-force` +- [x] 2.4 Create `src/types.ts` with `NodeType`, `NodeData`, `ProbeMessage`, `ReasoningNode` interfaces +- [x] 2.5 Rename `main.jsx` → `main.tsx`, add type annotations +- [x] 2.6 Rename `MathNode.jsx` → `MathNode.tsx`, type props with `@xyflow/react` node types +- [x] 2.7 Rename `App.jsx` → `App.tsx`, type WebSocket handler, d3-force simulation, and state +- [x] 2.8 Update `vite.config.js` if needed for TS support +- [x] 2.9 Verify `npm run build` succeeds + +## 3. MCP Mode + +- [ ] 3.1 Create `teacher_ai/mcp_server.py` with MCP server setup (stdio + SSE transport) +- [ ] 3.2 Implement `add_reasoning_node` tool handler +- [ ] 3.3 Implement `probe_node` tool handler with clipboard support +- [ ] 3.4 Implement `list_sessions` tool handler +- [ ] 3.5 Implement `load_session` tool handler +- [ ] 3.6 Implement `delete_session` tool handler +- [ ] 3.7 Implement `rename_session` tool handler +- [ ] 3.8 Embed system prompt in tool descriptions + +## 4. WebSocket Bridge (shared) + +- [ ] 4.1 Create `teacher_ai/websocket_server.py` with shared WS broadcast +- [ ] 4.2 Implement broadcast on `add_reasoning_node` call +- [ ] 4.3 Implement full graph state send on client connect +- [ ] 4.4 Implement session switch broadcast on `load_session` call +- [ ] 4.5 Update frontend WebSocket URL to point to local server + +## 5. Session Persistence (shared) + +- [x] 5.1 Create `teacher_ai/session_manager.py` with SQLite schema +- [x] 5.2 Implement `SessionManager` class with CRUD operations +- [x] 5.3 Wire auto-save into `add_reasoning_node` handler +- [x] 5.4 Implement last-session auto-restore on startup +- [x] 5.5 Create `teacher_ai/settings.py` for JSON settings store + +## 6. Frontend Updates + +- [ ] 6.1 Replace probe WebSocket send with clipboard copy (MCP mode only) +- [ ] 6.2 Add session sidebar component (`SessionSidebar.tsx`) +- [ ] 6.3 Add settings panel component (`SettingsPanel.tsx`) +- [ ] 6.4 Update `App.tsx` to handle full graph state on connect +- [ ] 6.5 Update `App.tsx` to handle session switch events + +## 7. Local Setup & Launch + +- [ ] 7.1 Add frontend build check + auto-build in `teacher_ai.py` startup +- [ ] 7.2 Add static file serving for built frontend +- [ ] 7.3 Add browser auto-launch on startup +- [ ] 7.4 Create `.env.example` with available config options +- [ ] 7.5 Add port fallback logic for WebSocket server +- [ ] 7.6 Test full flow: `python teacher_ai.py --mode mcp` → connect LLM client → ask question → see graph + +## 8. CI/CD Pipeline + +- [x] 8.1 Create `.github/workflows/ci.yml` with matrix (Python 3.11, 3.12) +- [x] 8.2 Write functional test suite (pytest): SQLite, WebSocket +- [ ] 8.3 Write integration test suite: MCP → WS → browser handshake +- [x] 8.4 Write adversarial test suite: flood, injection, XSS, RCE paths +- [x] 8.5 Write edge case test suite: large graphs, concurrent, corrupt DB +- [x] 8.6 Add frontend tests (vitest): component render, probe clipboard +- [x] 8.7 Add lint configs: ruff (Python), eslint (frontend) +- [x] 8.8 Add type check configs: tsc (frontend) +- [ ] 8.9 Add coverage reporting (pytest-cov, vitest --coverage) +- [ ] 8.10 Add rate limiter to WebSocket server (token-bucket, 30/sec) +- [ ] 8.11 Add input sanitization (content length limits, strip HTML) +- [ ] 8.12 Add subprocess path validation for npm build + +## 9. Security Audit Pipeline + +- [x] 9.1 Create `.github/workflows/audit.yml` (weekly schedule + PR trigger) +- [x] 9.2 Add pip-audit for Python dependency scanning +- [x] 9.3 Add npm audit for Node dependency scanning +- [x] 9.4 Add bandit for Python SAST +- [x] 9.5 Add truffleHog for secrets detection +- [ ] 9.6 Wire audit results to GitHub Issues for tracked findings + +## 10. Standalone Mode Refactor + +- [ ] 10.1 Extract `main.py` WebSocket + Gemini logic into `teacher_ai/standalone.py` using LiteLLM +- [ ] 10.2 Create `teacher_ai/websocket_server.py` shared WS broadcast module +- [ ] 10.3 Create `teacher_ai/__init__.py` +- [ ] 10.4 Wire standalone mode into `teacher_ai.py` entrypoint +- [ ] 10.5 Verify `python teacher_ai.py` works identically to `uvicorn main:app` +- [ ] 10.6 Add LiteLLM to `requirements.txt` diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..b4bbeb9 --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1 @@ +schema: spec-driven diff --git a/openspec/tests/__init__.py b/openspec/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openspec/tests/test_smoke.py b/openspec/tests/test_smoke.py new file mode 100644 index 0000000..91572dc --- /dev/null +++ b/openspec/tests/test_smoke.py @@ -0,0 +1,3 @@ +def test_imports(): + from main import app + assert app.title == "FastAPI" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fb357b0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] diff --git a/requirements.txt b/requirements.txt index c2ea2cd..53614f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,298 +1,4 @@ -accelerate==1.12.0 -aiocache==0.12.3 -aiofiles==25.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.2 -aiosignal==1.4.0 -alembic==1.17.2 -annotated-doc==0.0.4 -annotated-types==0.7.0 -anthropic==0.79.0 -anyio==4.12.1 -APScheduler==3.11.2 -argon2-cffi==25.1.0 -argon2-cffi-bindings==25.1.0 -asgiref==3.11.0 -async-timeout==5.0.1 -attrs==25.4.0 -Authlib==1.6.6 -av==16.1.0 -azure-ai-documentintelligence==1.0.2 -azure-core==1.38.0 -azure-identity==1.25.1 -azure-storage-blob==12.27.1 -backoff==2.2.1 -bcrypt==5.0.0 -beautifulsoup4==4.14.3 -bidict==0.23.1 -black==25.12.0 -boto3==1.42.21 -botocore==1.42.44 -brotli==1.2.0 -build==1.4.0 -certifi==2026.1.4 -cffi==2.0.0 -chardet==5.2.0 -charset-normalizer==3.4.4 -chromadb==1.4.0 -click==8.3.1 -cloudpickle==3.1.2 -coloredlogs==15.0.1 -contourpy==1.3.3 -cryptography==46.0.4 -ctranslate2==4.7.1 -cuda-bindings==12.9.4 -cuda-pathfinder==1.3.3 -cycler==0.12.1 -dataclasses-json==0.6.7 -ddgs==9.10.0 -defusedxml==0.7.1 -distro==1.9.0 -docstring_parser==0.17.0 -docx2txt==0.9 -durationpy==0.10 -ecdsa==0.19.1 -einops==0.8.1 -emoji==2.15.0 -et_xmlfile==2.0.0 -Events==0.5 -fake-useragent==2.2.0 -Farama-Notifications==0.0.4 fastapi==0.128.0 -faster-whisper==1.2.1 -filelock==3.20.3 -filetype==1.2.0 -flatbuffers==25.12.19 -fonttools==4.61.1 -fpdf2==2.8.5 -frozenlist==1.8.0 -fsspec==2026.2.0 -ftfy==6.3.1 -google-api-core==2.29.0 -google-api-python-client==2.189.0 -google-auth==2.49.1 -google-auth-httplib2==0.3.0 -google-auth-oauthlib==1.2.4 -google-cloud-core==2.5.0 -google-cloud-storage==3.7.0 -google-crc32c==1.8.0 google-genai==1.56.0 -google-resumable-media==2.8.0 -googleapis-common-protos==1.72.0 -greenlet==3.3.1 -grpcio==1.78.0 -gymnasium==1.2.3 -h11==0.16.0 -h2==4.3.0 -hf-xet==1.2.0 -hpack==4.1.0 -html5lib==1.1 -httpcore==1.0.9 -httplib2==0.31.2 -httptools==0.7.1 -httpx==0.28.1 -httpx-sse==0.4.3 -huggingface_hub==0.36.2 -humanfriendly==10.0 -hyperframe==6.1.0 -idna==3.11 -importlib_metadata==8.7.1 -importlib_resources==6.5.2 -isodate==0.7.2 -itsdangerous==2.2.0 -Jinja2==3.1.6 -jiter==0.13.0 -jmespath==1.1.0 -joblib==1.5.3 -jsonpatch==1.33 -jsonpointer==3.0.0 -jsonschema==4.26.0 -jsonschema-specifications==2025.9.1 -kiwisolver==1.4.9 -kubernetes==35.0.0 -langchain==1.2.0 -langchain-classic==1.0.1 -langchain-community==0.4.1 -langchain-core==1.2.9 -langchain-text-splitters==1.1.0 -langdetect==1.0.9 -langgraph==1.0.8 -langgraph-checkpoint==4.0.0 -langgraph-prebuilt==1.0.7 -langgraph-sdk==0.3.4 -langsmith==0.6.9 -ldap3==2.9.1 -loguru==0.7.3 -lxml==6.0.2 -Mako==1.3.10 -Markdown==3.10 -markdown-it-py==4.0.0 -MarkupSafe==3.0.3 -marshmallow==3.26.2 -matplotlib==3.10.8 -mcp==1.25.0 -mdurl==0.1.2 -mmh3==5.2.0 -mpmath==1.3.0 -msal==1.34.0 -msal-extensions==1.3.1 -msoffcrypto-tool==5.4.2 -multidict==6.7.1 -mypy_extensions==1.1.0 -networkx==3.6.1 -nltk==3.9.2 -numpy==2.2.6 -nvidia-cublas-cu12==12.8.4.1 -nvidia-cuda-cupti-cu12==12.8.90 -nvidia-cuda-nvrtc-cu12==12.8.93 -nvidia-cuda-runtime-cu12==12.8.90 -nvidia-cudnn-cu12==9.10.2.21 -nvidia-cufft-cu12==11.3.3.83 -nvidia-cufile-cu12==1.13.1.3 -nvidia-curand-cu12==10.3.9.90 -nvidia-cusolver-cu12==11.7.3.90 -nvidia-cusparse-cu12==12.5.8.93 -nvidia-cusparselt-cu12==0.7.1 -nvidia-nccl-cu12==2.27.5 -nvidia-nvjitlink-cu12==12.8.93 -nvidia-nvshmem-cu12==3.4.5 -nvidia-nvtx-cu12==12.8.90 -oauthlib==3.3.1 -olefile==0.47 -onnxruntime==1.23.2 -open-webui==0.7.2 -openai==2.17.0 -opencv-python==4.13.0.92 -opencv-python-headless==4.12.0.88 -openpyxl==3.1.5 -opensearch-protobufs==0.19.0 -opensearch-py==3.1.0 -opentelemetry-api==1.39.1 -opentelemetry-exporter-otlp-proto-common==1.39.1 -opentelemetry-exporter-otlp-proto-grpc==1.39.1 -opentelemetry-proto==1.39.1 -opentelemetry-sdk==1.39.1 -opentelemetry-semantic-conventions==0.60b1 -orjson==3.11.7 -ormsgpack==1.12.2 -overrides==7.7.0 -packaging==26.0 -pandas==2.3.3 -pathspec==1.0.4 -peewee==3.18.3 -peewee-migrate==1.14.3 -pillow==12.1.0 -platformdirs==4.5.1 -posthog==5.4.0 -primp==0.15.0 -propcache==0.4.1 -proto-plus==1.27.1 -protobuf==6.33.5 -psutil==7.2.2 -pyarrow==20.0.0 -pyasn1==0.6.2 -pyasn1_modules==0.4.2 -pybase64==1.4.3 -pyclipper==1.4.0 -pycparser==3.0 -pycrdt==0.12.44 -pydantic==2.12.5 -pydantic-settings==2.12.0 -pydantic_core==2.41.5 -pydub==0.25.1 -Pygments==2.19.2 -PyJWT==2.10.1 -pymdown-extensions==10.20 -PyMySQL==1.1.2 -pypandoc==1.16.2 -pyparsing==3.3.2 -pypdf==6.5.0 -pypdfium2==5.3.0 -PyPika==0.51.1 -pyproject_hooks==1.2.0 -python-dateutil==2.9.0.post0 -python-dotenv==1.2.1 -python-engineio==4.13.1 -python-iso639==2026.1.31 -python-jose==3.5.0 -python-magic==0.4.27 -python-mimeparse==2.0.0 -python-multipart==0.0.21 -python-oxmsg==0.0.2 -python-pptx==1.0.2 -python-socketio==5.16.0 -pytokens==0.4.1 -pytube==15.0.0 -pytz==2025.2 -pyxlsb==1.0.10 -PyYAML==6.0.3 -rank-bm25==0.2.2 -RapidFuzz==3.14.3 -rapidocr-onnxruntime==1.4.4 -redis==7.1.0 -referencing==0.37.0 -regex==2026.1.15 -requests==2.32.5 -requests-oauthlib==2.0.0 -requests-toolbelt==1.0.0 -RestrictedPython==8.1 -rich==13.9.4 -rpds-py==0.30.0 -rsa==4.9.1 -s3transfer==0.16.0 -safetensors==0.7.0 -scikit-learn==1.8.0 -scipy==1.17.0 -sentence-transformers==5.2.0 -sentencepiece==0.2.1 -shapely==2.1.2 -shellingham==1.5.4 -simple-websocket==1.1.0 -six==1.17.0 -sniffio==1.3.1 -socksio==1.0.0 -soundfile==0.13.1 -soupsieve==2.8.3 -SQLAlchemy==2.0.45 -sse-starlette==3.2.0 -stable_baselines3==2.7.1 -starlette==0.50.0 -starlette-compress==1.6.1 -starsessions==2.2.1 -sympy==1.14.0 -tenacity==9.1.4 -threadpoolctl==3.6.0 -tiktoken==0.12.0 -tokenizers==0.22.2 -torch==2.10.0 -tqdm==4.67.3 -transformers==4.57.3 -triton==3.6.0 -typer==0.21.1 -typing-inspect==0.9.0 -typing-inspection==0.4.2 -typing_extensions==4.15.0 -tzdata==2025.3 -tzlocal==5.3.1 -unstructured==0.18.24 -unstructured-client==0.42.10 -uritemplate==4.2.0 -urllib3==2.6.3 -uuid_utils==0.14.0 +litellm==1.63.0 uvicorn==0.40.0 -uvloop==0.22.1 -validators==0.35.0 -watchfiles==1.1.1 -wcwidth==0.6.0 -webencodings==0.5.1 -websocket-client==1.9.0 -websockets==15.0.1 -wrapt==2.1.1 -wsproto==1.3.2 -xlrd==2.0.2 -xlsxwriter==3.2.9 -xxhash==3.6.0 -yarl==1.22.0 -youtube-transcript-api==1.2.3 -zipp==3.23.0 -zstandard==0.25.0 diff --git a/teacher_ai.py b/teacher_ai.py new file mode 100644 index 0000000..0ebfa9e --- /dev/null +++ b/teacher_ai.py @@ -0,0 +1,38 @@ +# Dual-mode entrypoint for Teacher AI Explainer. +# +# Usage: +# python teacher_ai.py # standalone mode (default) +# python teacher_ai.py --mode standalone +# python teacher_ai.py --mode mcp # MCP mode (not yet implemented) +# +# Standalone mode: FastAPI + WebSocket + LiteLLM. The server calls the LLM directly. +# MCP mode: MCP server + WebSocket bridge. The user's LLM client drives the conversation. + +import argparse +import sys + + +def main(): + parser = argparse.ArgumentParser(description="Teacher AI Explainer") + parser.add_argument( + "--mode", + choices=["standalone", "mcp"], + default="standalone", + help="Run mode (default: standalone)", + ) + args = parser.parse_args() + + if args.mode == "standalone": + import uvicorn + + from teacher_ai.standalone import app + + port = int(sys.argv[sys.argv.index("--port") + 1]) if "--port" in sys.argv else 8000 + uvicorn.run(app, host="0.0.0.0", port=port) + elif args.mode == "mcp": + print("MCP mode is not yet implemented. Use --mode standalone.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/teacher_ai/__init__.py b/teacher_ai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/teacher_ai/session_manager.py b/teacher_ai/session_manager.py new file mode 100644 index 0000000..86c01be --- /dev/null +++ b/teacher_ai/session_manager.py @@ -0,0 +1,166 @@ +# SQLite-backed session persistence for reasoning graphs. +# Auto-saves nodes and edges per session, supports browse, resume, and delete. + +import sqlite3 +import threading +import uuid +from datetime import datetime, timezone +from pathlib import Path + +DB_DIR = Path.home() / ".teacher-ai-explainer" +DB_PATH = DB_DIR / "sessions.db" + +# Schema: sessions, nodes, edges +SCHEMA = """ +CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT 'New Session', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS nodes ( + id TEXT NOT NULL, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + node_type TEXT NOT NULL, + label TEXT NOT NULL, + content TEXT NOT NULL, + parent_id TEXT, + position_x REAL, + position_y REAL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (id, session_id) +); + +CREATE TABLE IF NOT EXISTS edges ( + id TEXT NOT NULL, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + source TEXT NOT NULL, + target TEXT NOT NULL, + PRIMARY KEY (id, session_id) +); +""" + + +class SessionManager: + def __init__(self, db_path: str | Path = DB_PATH): + self._db_path = Path(db_path) + self._db_path.parent.mkdir(parents=True, exist_ok=True) + self._local = threading.local() + + def _conn(self) -> sqlite3.Connection: + if not hasattr(self._local, "conn") or self._local.conn is None: + self._local.conn = sqlite3.connect(str(self._db_path)) + self._local.conn.row_factory = sqlite3.Row + self._local.conn.execute("PRAGMA journal_mode=WAL") + self._local.conn.execute("PRAGMA foreign_keys=ON") + self._local.conn.executescript(SCHEMA) + return self._local.conn + + # --- Sessions --- + + def create_session(self, title: str = "New Session") -> dict: + session_id = str(uuid.uuid4()) + now = datetime.now(timezone.utc).isoformat() + self._conn().execute( + "INSERT INTO sessions (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)", + (session_id, title, now, now), + ) + self._conn().commit() + return self.get_session(session_id) + + def get_session(self, session_id: str) -> dict | None: + row = self._conn().execute( + "SELECT id, title, created_at, updated_at FROM sessions WHERE id = ?", + (session_id,), + ).fetchone() + if row is None: + return None + return dict(row) + + def list_sessions(self) -> list[dict]: + rows = self._conn().execute( + "SELECT s.id, s.title, s.created_at, s.updated_at, " + "COUNT(n.id) AS node_count " + "FROM sessions s LEFT JOIN nodes n ON n.session_id = s.id " + "GROUP BY s.id ORDER BY s.updated_at DESC" + ).fetchall() + return [dict(r) for r in rows] + + def rename_session(self, session_id: str, title: str) -> bool: + now = datetime.now(timezone.utc).isoformat() + cur = self._conn().execute( + "UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?", + (title, now, session_id), + ) + self._conn().commit() + return cur.rowcount > 0 + + def delete_session(self, session_id: str) -> bool: + cur = self._conn().execute("DELETE FROM sessions WHERE id = ?", (session_id,)) + self._conn().commit() + return cur.rowcount > 0 + + # --- Nodes --- + + def add_node( + self, + session_id: str, + node_id: str, + node_type: str, + label: str, + content: str, + parent_id: str | None = None, + position_x: float | None = None, + position_y: float | None = None, + ) -> bool: + now = datetime.now(timezone.utc).isoformat() + self._conn().execute( + "INSERT OR IGNORE INTO nodes (id, session_id, node_type, label, content, " + "parent_id, position_x, position_y, created_at) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + (node_id, session_id, node_type, label, content, + parent_id, position_x, position_y, now), + ) + # Touch session updated_at + self._conn().execute( + "UPDATE sessions SET updated_at = ? WHERE id = ?", (now, session_id) + ) + self._conn().commit() + return True + + def get_nodes(self, session_id: str) -> list[dict]: + rows = self._conn().execute( + "SELECT id, node_type, label, content, parent_id, position_x, position_y " + "FROM nodes WHERE session_id = ? ORDER BY created_at", + (session_id,), + ).fetchall() + return [dict(r) for r in rows] + + # --- Edges --- + + def add_edge(self, session_id: str, edge_id: str, source: str, target: str) -> bool: + self._conn().execute( + "INSERT OR IGNORE INTO edges (id, session_id, source, target) VALUES (?, ?, ?, ?)", + (edge_id, session_id, source, target), + ) + self._conn().commit() + return True + + def get_edges(self, session_id: str) -> list[dict]: + rows = self._conn().execute( + "SELECT id, source, target FROM edges WHERE session_id = ?", (session_id,) + ).fetchall() + return [dict(r) for r in rows] + + # --- Full graph --- + + def get_full_graph(self, session_id: str) -> dict | None: + session = self.get_session(session_id) + if session is None: + return None + return { + **session, + "nodes": self.get_nodes(session_id), + "edges": self.get_edges(session_id), + } diff --git a/teacher_ai/settings.py b/teacher_ai/settings.py new file mode 100644 index 0000000..4dc0b47 --- /dev/null +++ b/teacher_ai/settings.py @@ -0,0 +1,40 @@ +# JSON settings store — persists user preferences across restarts. +# Stored at ~/.teacher-ai-explainer/settings.json + +import json +from pathlib import Path + +SETTINGS_PATH = Path.home() / ".teacher-ai-explainer" / "settings.json" + +DEFAULT_SETTINGS = { + "last_session_id": None, + "ws_port": 8000, + "browser_auto_launch": True, +} + + +class Settings: + def __init__(self, path: Path = SETTINGS_PATH): + self._path = path + self._path.parent.mkdir(parents=True, exist_ok=True) + self._data = dict(DEFAULT_SETTINGS) + self._load() + + def _load(self): + if self._path.exists(): + try: + with open(self._path) as f: + self._data.update(json.load(f)) + except (json.JSONDecodeError, OSError): + pass + + def _save(self): + with open(self._path, "w") as f: + json.dump(self._data, f, indent=2) + + def get(self, key: str, default=None): + return self._data.get(key, default) + + def set(self, key: str, value): + self._data[key] = value + self._save() diff --git a/teacher_ai/standalone.py b/teacher_ai/standalone.py new file mode 100644 index 0000000..45ba9f0 --- /dev/null +++ b/teacher_ai/standalone.py @@ -0,0 +1,205 @@ +# Standalone mode — FastAPI + WebSocket server that uses LiteLLM to call any LLM provider. +# The server owns the LLM call loop: user types in browser → server calls LLM → nodes stream back. +# Supports OpenAI, Anthropic, Gemini, Ollama, and 100+ providers. +# All nodes and edges are auto-saved to SQLite for session persistence. +# Supports cancel: send {"type": "cancel"} to stop the current LLM inference mid-stream. + +import asyncio +import json +import os + +from fastapi import FastAPI, WebSocket +from fastapi.middleware.cors import CORSMiddleware + +os.environ["LITELLM_TELEMETRY"] = "False" + +from litellm import completion + +from teacher_ai.session_manager import SessionManager +from teacher_ai.settings import Settings + +DEFAULT_PROVIDER = "gemini" +DEFAULT_MODEL = "gemini-2.5-pro" + +app = FastAPI() + +origins = [ + "http://localhost:5173", + "https://cs598-project-492002.web.app", + "https://cs598-project-492002.firebaseapp.com", +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +tools = [ + { + "type": "function", + "function": { + "name": "add_reasoning_node", + "description": "Adds an atomic step of reasoning to the interactive whiteboard graph.", + "parameters": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "label": {"type": "string"}, + "content": {"type": "string", "description": "LaTeX math content."}, + "node_type": { + "type": "string", + "enum": [ + "Given", "Objective", "Principle", "Derivation", + "Self-Correction", "Alternative", "Final Answer", + ], + }, + "parent_id": {"type": "string"}, + }, + "required": ["id", "label", "content", "node_type"], + }, + }, + } +] + +provider = os.environ.get("LLM_PROVIDER", DEFAULT_PROVIDER) +model_name = os.environ.get("MODEL_NAME", DEFAULT_MODEL) +model = f"{provider}/{model_name}" +api_key = os.environ.get("LLM_API_KEY", None) +api_base = os.environ.get("LLM_URL", None) +request_timeout = int(os.environ.get("LLM_TIMEOUT", "120")) + +session_mgr = SessionManager() +settings = Settings() + + +@app.get("/") +async def health(): + return {"status": "Professor is in"} + + +@app.websocket("/ws/reason") +async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + + with open("System_Prompt.md", "r") as f: + sys_inst = f.read() + + sess = session_mgr.create_session(title="New Reasoning Session") + session_id = sess["id"] + settings.set("last_session_id", session_id) + + messages = [{"role": "system", "content": sys_inst}] + current_task: asyncio.Task | None = None + + try: + while True: + raw = await websocket.receive_text() + + # Handle cancel — stop the current LLM inference + try: + msg = json.loads(raw) + if msg.get("type") == "cancel": + if current_task and not current_task.done(): + current_task.cancel() + await websocket.send_json({"type": "cancelled"}) + continue + except (json.JSONDecodeError, AttributeError): + pass + + # Route: probe vs regular query + try: + msg = json.loads(raw) + is_probe = msg.get("type") == "probe" + except (json.JSONDecodeError, AttributeError): + msg = None + is_probe = False + + if is_probe: + parent_id = msg["parent_id"] + node_content = msg["content"] + student_query = ( + f"A student is confused about this specific step on the whiteboard:\n\n" + f"{node_content}\n\n" + f"Explain this step in a different way using a single Alternative node. " + f"You MUST set node_type to 'Alternative' and parent_id to '{parent_id}'. " + f"Do not add any other nodes." + ) + else: + student_query = raw + + messages.append({"role": "user", "content": student_query}) + + # Run the LLM call loop in a cancellable task + async def run_inference(): + nonlocal messages + kwargs = dict( + model=model, + api_key=api_key, + messages=messages, + tools=tools, + temperature=0.1, + timeout=request_timeout, + ) + if api_base: + kwargs["api_base"] = api_base + + response = completion(**kwargs) + + while True: + choice = response.choices[0] + message = choice.message + + if not message.tool_calls: + break + + for tool_call in message.tool_calls: + node_data = json.loads(tool_call.function.arguments) + + if is_probe: + node_data["node_type"] = "Alternative" + node_data["parent_id"] = parent_id + + session_mgr.add_node( + session_id=session_id, + node_id=str(node_data["id"]), + node_type=node_data["node_type"], + label=node_data.get("label", ""), + content=node_data.get("content", ""), + parent_id=node_data.get("parent_id"), + ) + + if node_data.get("parent_id"): + edge_id = f"e-{node_data['parent_id']}-{node_data['id']}" + session_mgr.add_edge( + session_id=session_id, + edge_id=edge_id, + source=str(node_data["parent_id"]), + target=str(node_data["id"]), + ) + + await asyncio.sleep(1.2) + await websocket.send_json(node_data) + print(f"Node sent: {node_data.get('label')} [{node_data.get('node_type')}]") + + messages.append(message.model_dump()) + messages.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": json.dumps( + {"status": "success", "message": "Node rendered on whiteboard"} + ), + }) + + response = completion(**kwargs) + + current_task = asyncio.create_task(run_inference()) + try: + await current_task + except asyncio.CancelledError: + print("Inference cancelled by user") + await websocket.send_json({"type": "cancelled"}) + + except Exception as e: + print(f"WS Error: {e}") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_adversarial.py b/tests/test_adversarial.py new file mode 100644 index 0000000..c09746a --- /dev/null +++ b/tests/test_adversarial.py @@ -0,0 +1,110 @@ +# Adversarial tests — security boundaries: injection, XSS, large payloads, malformed input. + +import json +from unittest.mock import MagicMock, patch + +from fastapi.testclient import TestClient + +from teacher_ai.standalone import app + + +def _make_mock_pair(): + first = MagicMock() + first.choices = [ + MagicMock( + message=MagicMock( + tool_calls=[ + MagicMock( + id="call_1", + function=MagicMock( + arguments=json.dumps({ + "id": "n1", + "label": "Step", + "content": "ok", + "node_type": "Given", + }) + ), + ) + ], + model_dump=lambda: {"role": "assistant", "content": None, "tool_calls": []}, + ) + ) + ] + second = MagicMock() + second.choices = [ + MagicMock( + message=MagicMock( + tool_calls=None, + model_dump=lambda: {"role": "assistant", "content": "done"}, + ) + ) + ] + return first, second + + +@patch("teacher_ai.standalone.completion") +def test_sql_injection_in_node_content(mock_completion): + mock_completion.side_effect = _make_mock_pair() + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text(json.dumps({ + "type": "probe", + "parent_id": "1", + "content": "'; DROP TABLE nodes; --", + })) + data = ws.receive_json() + assert data["id"] == "n1" + + +@patch("teacher_ai.standalone.completion") +def test_xss_injection_in_query(mock_completion): + mock_completion.side_effect = _make_mock_pair() + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text("") + data = ws.receive_json() + assert data["id"] == "n1" + + +@patch("teacher_ai.standalone.completion") +def test_large_payload(mock_completion): + mock_completion.side_effect = _make_mock_pair() + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text(json.dumps({ + "type": "probe", + "parent_id": "1", + "content": "A" * 100_000, + })) + data = ws.receive_json() + assert data["id"] == "n1" + + +@patch("teacher_ai.standalone.completion") +def test_malformed_json(mock_completion): + mock_completion.side_effect = _make_mock_pair() + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text("not valid json") + data = ws.receive_json() + assert data["id"] == "n1" + + +@patch("teacher_ai.standalone.completion") +def test_empty_message(mock_completion): + mock_completion.side_effect = _make_mock_pair() + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text("") + data = ws.receive_json() + assert data["id"] == "n1" + + +@patch("teacher_ai.standalone.completion") +def test_unicode_injection(mock_completion): + mock_completion.side_effect = _make_mock_pair() + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text("\u0000\u0008\u001f\u007f") + data = ws.receive_json() + assert data["id"] == "n1" diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py new file mode 100644 index 0000000..8eae043 --- /dev/null +++ b/tests/test_edge_cases.py @@ -0,0 +1,64 @@ +# Edge case tests — concurrent connections, large graph sequences, and extreme conditions. + +import json +from unittest.mock import MagicMock, patch + +from fastapi.testclient import TestClient + +from teacher_ai.standalone import app + + +def _make_mock_pair(): + first = MagicMock() + first.choices = [ + MagicMock( + message=MagicMock( + tool_calls=[ + MagicMock( + id="call_0", + function=MagicMock( + arguments=json.dumps({ + "id": "n0", + "label": "Step 0", + "content": "$x_0 = 0$", + "node_type": "Derivation", + }) + ), + ) + ], + model_dump=lambda: {"role": "assistant", "content": None, "tool_calls": []}, + ) + ) + ] + second = MagicMock() + second.choices = [ + MagicMock( + message=MagicMock( + tool_calls=None, + model_dump=lambda: {"role": "assistant", "content": "done"}, + ) + ) + ] + return first, second + + +def test_multiple_sequential_connections(): + """Open 5 WebSocket connections one after another — all should connect and handle cancel.""" + client = TestClient(app) + for i in range(5): + with client.websocket_connect("/ws/reason") as ws: + ws.send_text(json.dumps({"type": "cancel"})) + data = ws.receive_json() + assert data["type"] == "cancelled" + + +@patch("teacher_ai.standalone.completion") +def test_large_graph_sequence(mock_completion): + """Send a query that triggers a tool call — verify the node is returned correctly.""" + mock_completion.side_effect = _make_mock_pair() + + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text("Build a graph") + data = ws.receive_json() + assert data["id"] == "n0" diff --git a/tests/test_session_manager.py b/tests/test_session_manager.py new file mode 100644 index 0000000..7dee91c --- /dev/null +++ b/tests/test_session_manager.py @@ -0,0 +1,88 @@ +"""Tests for SQLite session persistence — CRUD operations on sessions, nodes, and edges.""" + +import tempfile +from pathlib import Path + +from teacher_ai.session_manager import SessionManager + + +def test_create_and_get_session(): + """Create a session with a custom title and verify it can be retrieved by ID.""" + with tempfile.TemporaryDirectory() as tmp: + mgr = SessionManager(Path(tmp) / "test.db") + sess = mgr.create_session("Test Session") + assert sess["title"] == "Test Session" + assert sess["id"] is not None + + fetched = mgr.get_session(sess["id"]) + assert fetched["title"] == "Test Session" + + +def test_add_and_get_nodes(): + """Add two nodes (one with a parent reference) and verify they are stored and retrievable.""" + with tempfile.TemporaryDirectory() as tmp: + mgr = SessionManager(Path(tmp) / "test.db") + sess = mgr.create_session() + mgr.add_node(sess["id"], "n1", "Given", "Step 1", "$x = 1$") + mgr.add_node(sess["id"], "n2", "Derivation", "Step 2", "$x = 2$", parent_id="n1") + + nodes = mgr.get_nodes(sess["id"]) + assert len(nodes) == 2 + assert nodes[0]["node_type"] == "Given" + assert nodes[1]["parent_id"] == "n1" + + +def test_add_and_get_edges(): + """Add an edge between two nodes and verify it is stored correctly.""" + with tempfile.TemporaryDirectory() as tmp: + mgr = SessionManager(Path(tmp) / "test.db") + sess = mgr.create_session() + mgr.add_edge(sess["id"], "e-1-2", "n1", "n2") + edges = mgr.get_edges(sess["id"]) + assert len(edges) == 1 + assert edges[0]["source"] == "n1" + + +def test_list_sessions(): + """Create multiple sessions and verify list_sessions returns all of them.""" + with tempfile.TemporaryDirectory() as tmp: + mgr = SessionManager(Path(tmp) / "test.db") + mgr.create_session("First") + mgr.create_session("Second") + sessions = mgr.list_sessions() + assert len(sessions) == 2 + + +def test_delete_session(): + """Delete a session and verify its nodes are also removed (cascade delete).""" + with tempfile.TemporaryDirectory() as tmp: + mgr = SessionManager(Path(tmp) / "test.db") + sess = mgr.create_session() + mgr.add_node(sess["id"], "n1", "Given", "Step 1", "$x$") + assert mgr.delete_session(sess["id"]) is True + assert mgr.get_session(sess["id"]) is None + assert len(mgr.get_nodes(sess["id"])) == 0 + + +def test_rename_session(): + """Rename a session and verify the title is updated.""" + with tempfile.TemporaryDirectory() as tmp: + mgr = SessionManager(Path(tmp) / "test.db") + sess = mgr.create_session("Old") + mgr.rename_session(sess["id"], "Renamed") + assert mgr.get_session(sess["id"])["title"] == "Renamed" + + +def test_full_graph(): + """Build a small graph with nodes and edges, then verify get_full_graph returns everything.""" + with tempfile.TemporaryDirectory() as tmp: + mgr = SessionManager(Path(tmp) / "test.db") + sess = mgr.create_session("Graph Test") + mgr.add_node(sess["id"], "n1", "Given", "Start", "$a$") + mgr.add_node(sess["id"], "n2", "Derivation", "Next", "$b$", parent_id="n1") + mgr.add_edge(sess["id"], "e-n1-n2", "n1", "n2") + + graph = mgr.get_full_graph(sess["id"]) + assert graph is not None + assert len(graph["nodes"]) == 2 + assert len(graph["edges"]) == 1 diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..126bf1b --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,17 @@ +"""Smoke test — verifies the FastAPI apps import and initialize correctly.""" + +import os + + +def test_standalone_imports(): + """Verify the standalone app can be imported without errors.""" + from teacher_ai.standalone import app + assert app.title == "FastAPI" + + +def test_legacy_imports(): + """Verify the legacy main.py app imports (skipped if GCP env vars not set).""" + if not os.environ.get("GOOGLE_CLOUD_PROJECT") or not os.environ.get("GOOGLE_CLOUD_LOCATION"): + return + from main import app + assert app.title == "FastAPI" diff --git a/tests/test_websocket.py b/tests/test_websocket.py new file mode 100644 index 0000000..1222eef --- /dev/null +++ b/tests/test_websocket.py @@ -0,0 +1,105 @@ +"""Tests for WebSocket functionality — standalone mode LiteLLM integration. +Uses mocked LiteLLM to avoid real API calls.""" + +import json +from unittest.mock import MagicMock, patch + +from fastapi.testclient import TestClient + +from teacher_ai.standalone import app + + +def test_health_endpoint(): + """Verify the health check endpoint returns the expected status.""" + client = TestClient(app) + resp = client.get("/") + assert resp.status_code == 200 + assert resp.json() == {"status": "Professor is in"} + + +def test_websocket_accepts_connection(): + """Verify the WebSocket endpoint accepts a connection without error.""" + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + assert ws + + +def test_websocket_receives_cancelled_on_cancel(): + """Send a cancel message and verify the server responds with cancelled confirmation.""" + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text(json.dumps({"type": "cancel"})) + data = ws.receive_json() + assert data["type"] == "cancelled" + + +@patch("teacher_ai.standalone.completion") +def test_websocket_sends_node_on_query(mock_completion): + """Send a regular query and verify the server returns a reasoning node from the mocked LLM.""" + mock_response = MagicMock() + mock_response.choices = [ + MagicMock( + message=MagicMock( + tool_calls=[ + MagicMock( + id="call_1", + function=MagicMock( + arguments=json.dumps({ + "id": "n1", + "label": "Step 1", + "content": "$x = 5$", + "node_type": "Given", + }) + ), + ) + ], + model_dump=lambda: {"role": "assistant", "content": None, "tool_calls": []}, + ) + ) + ] + mock_completion.return_value = mock_response + + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text("Solve for x") + data = ws.receive_json() + assert data["id"] == "n1" + assert data["node_type"] == "Given" + + +@patch("teacher_ai.standalone.completion") +def test_websocket_probe_returns_alternative(mock_completion): + """Send a probe — server forces node_type to Alternative with correct parent_id.""" + mock_response = MagicMock() + mock_response.choices = [ + MagicMock( + message=MagicMock( + tool_calls=[ + MagicMock( + id="call_1", + function=MagicMock( + arguments=json.dumps({ + "id": "n2", + "label": "Alt Step", + "content": "$y = 3$", + "node_type": "Alternative", + }) + ), + ) + ], + model_dump=lambda: {"role": "assistant", "content": None, "tool_calls": []}, + ) + ) + ] + mock_completion.return_value = mock_response + + client = TestClient(app) + with client.websocket_connect("/ws/reason") as ws: + ws.send_text(json.dumps({ + "type": "probe", + "parent_id": "n1", + "content": "$x = 5$", + })) + data = ws.receive_json() + assert data["node_type"] == "Alternative" + assert data["parent_id"] == "n1" diff --git a/whiteboard-dashboard/.firebase/hosting.ZGlzdA.cache b/whiteboard-dashboard/.firebase/hosting.ZGlzdA.cache deleted file mode 100644 index cc02076..0000000 --- a/whiteboard-dashboard/.firebase/hosting.ZGlzdA.cache +++ /dev/null @@ -1,64 +0,0 @@ -index.html,1775251974219,36eef49eef5073e35434dd8f738f01e4ef9095c3ad4c8a3d59a7154fd5b9485e -icons.svg,1775251973940,4fc6977ff6b4aa9bb88ef3244169aaa400418f25e0b811f4f91955a474bad6ad -favicon.svg,1775251973940,b44396fc895b603355bfbc7a0384483d188dbc841a0dfe6331f548f784f6d634 -assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2,1775251974219,1fcb14c53ae19dae27342c25197816961596389eb5c3b343ded691a6260a3526 -assets/KaTeX_Size4-Regular-BF-4gkZK.woff,1775251974219,9fc3e6465a91099eb725b2c605decf21f7c204688412916b1f22d3ccd894f795 -assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2,1775251974219,eb3b8f3bb3252c7b063265ac5ab436fd69ea97df3adeafca9e70ad6047ea953a -assets/KaTeX_Size3-Regular-DgpXs0kz.ttf,1775251974219,0a0aef269a76f60459abbd766a20835f842153aca0aba832e9c9fde3642559e1 -assets/KaTeX_Size3-Regular-CTq5MqoE.woff,1775251974218,574a2e300cb6103520741ec190e18fc83d33aa5ccf8752e23fa3b59f33084e2f -assets/KaTeX_Size2-Regular-oD1tc_U0.woff,1775251974218,76aaf671f946cf30f7adf329cdd67310b9fb8f07915c290bc339f754bbbd67e5 -assets/KaTeX_Size4-Regular-DWFBv043.ttf,1775251974219,8fcd2f3ee9a07b2ce68f54081b5e535937611641a69afac7158463929291db3d -assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff,1775251974219,5d05b8cc7998ef1c44147ef69b1377735a6307e036a1c4f469c9722bb9b8dd7b -assets/KaTeX_Size2-Regular-Dy4dx90m.woff2,1775251974218,eacfd70a470d73dd4850b2c7c9c42ca64991d9bca1e96e7d7b92894a4ba28afc -assets/index-BCIgSN4L.css,1775251974219,15f72836baab89dde1f9678e1fc4c317df19ebc0fac88b47233b89b51070766a -assets/KaTeX_Size1-Regular-mCD8mA8B.woff2,1775251974218,cc659328a72ef539df1b98559dbb3fa2218d348d8e75ef5bc01829c8ddf22a59 -assets/KaTeX_Size1-Regular-C195tn64.woff,1775251974218,f106c6d5c04dcae17b934f4322050bb682ec9486d01b0273719a3e67de34bf06 -assets/KaTeX_Script-Regular-D3wIWfF6.woff2,1775251974218,39bfeb766ad116f5921810dc0a4f2a06070b86b37a45f68d7041e47a4369120f -assets/KaTeX_Size2-Regular-B7gKUWhC.ttf,1775251974218,f894c70b498acc4ef1f94dc59d71389b30f7d1d86bb3388e77cc8ca1c3cb6bd0 -assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2,1775251974218,88219a057ae96cb3e7e137c11e0b774bfe7c6918a0cda71d19afcc7767014915 -assets/KaTeX_Size1-Regular-Dbsnue_I.ttf,1775251974218,f50475c194440aa98e1ea401193e75b687898fa53b86af13564217f3a59413f2 -assets/KaTeX_Script-Regular-D5yQViql.woff,1775251974218,f79f6df24d87450b3c0f2b74bfc116d6444b53050df317ff3768a34bc97a02a3 -assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff,1775251974218,ee4238399573fc684e0e7cb54c23492a3bbbac018e3484b3215604f9b3a916e2 -assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2,1775251974218,ee431488cd78fbd04b1936f35f972b9f946544e60f39d8b5636f6ececfba9306 -assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf,1775251974218,07c223ad2595479aea4528c8e76fc270096305b94cc8d32f0e8abbf219273979 -assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2,1775251974218,1d3bb81f336bc2a1978b0830ae34b1a42558591ee475d9f84d921c2c78c08110 -assets/KaTeX_SansSerif-Italic-DN2j7dab.woff,1775251974218,c9745f164ec219948dd53b126dff4c37f4cccce625ea569c41fd266432cdca17 -assets/KaTeX_Script-Regular-C5JkGWo-.ttf,1775251974218,1cafa21d4a2f183c58c52397fd1832e7449f2d28086000ddd5c43c96242932e1 -assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff,1775251974218,696cd311b268ce95b0d890898d48844be3d420cc420248c4a1aa27be0261a88f -assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf,1775251974218,08776278e8d7152865c23bbdc1cdde93c264b61ae02d1f21ac5f388027ad463b -assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf,1775251974218,cb36dad9464d13c90de81e059127ad2ee440316356d24f85168a16ea314adba4 -assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff,1775251974218,ddc18297cefbba16414abf5e384466ed1b466f6a6fa80cca952e4bcd9b2f2675 -assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf,1775251974219,3de89b51a2852854763f1a9b8df89317d90646318a5dc20806566735b98df75b -assets/KaTeX_Math-Italic-t53AETM-.woff2,1775251974218,f3c4404c5bf0ad00b23a2d468238181162ed1619e7d867f3c5af8ac1a0526b7e -assets/KaTeX_Math-Italic-DA0__PXp.woff,1775251974218,6dc14f93887af6e1b2bf2f2c4b84610a2e986e7f5e31f50b2bc63faf23405e81 -assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2,1775251974218,cd802cd1eaf0a69ae22a87565ce165c31b7685e823a865895d14eedf18530ac5 -assets/KaTeX_Math-Italic-flOr_0UB.ttf,1775251974218,4e9d2f573fc33d6b5c9a05bafecb734e420d0e8acff7b85c476cae5c415ab09b -assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff,1775251974218,a461527c0bd18be2428bb313f7fcfb41575b19895016a92a2a961a6c53317f2b -assets/KaTeX_Main-Regular-B22Nviop.woff2,1775251974218,99a5acea66d5280c13d85c06451a9a36124b68c24c26f1f7999d75c116dd1694 -assets/KaTeX_Main-Italic-NWA7e6Wa.woff2,1775251974218,f86a8492d2833704fb4a372667131cbff2c291df453bce8b6df1ee71204d127c -assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf,1775251974218,835dfb88d32886758eaa1670767db3c26e8d55388554e164f315f77e353e771c -assets/KaTeX_Main-Italic-BMLOBm91.woff,1775251974218,f1758a9b9f79d5ba66d7ff034633cabbfb559354e57e145be9d489d62c3aeb72 -assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2,1775251974218,6cfbfc9947bd3e2267b18b8aa5cf31f584368b8ab807a0eba52fe54ba7f8e8c1 -assets/KaTeX_Main-Regular-Dr94JaBh.woff,1775251974218,6bb37488ebd0a587062a4b088ae0210b406b38d485afaa7d7de58c5310892993 -assets/KaTeX_Main-BoldItalic-SpSLRI95.woff,1775251974218,4eff65d36aaa7aca47ba3116e4fbe8238fc50170dac1da396d1e927a2acc0e8c -assets/KaTeX_Main-Italic-3WenGoN9.ttf,1775251974218,a70916e80cf69e124606a180d798e3d573bd4a88c29e8cc8fd8b1f1702e29cfc -assets/KaTeX_Main-Bold-Cx986IdX.woff2,1775251974218,0d556ae3c9ab0185d507e9454134c028a930c9a2516b90a0afe996df51b7a614 -assets/KaTeX_Main-Bold-Jm3AIy58.woff,1775251974218,04b16804e648ae1b39d1e155e2b1c4e6e3337421b7b10b0f886be523aaa1c22e -assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf,1775251974218,e98af3a10827a8a02abdb86a457aa17b5546bc6667ba6049a33dfc5475caf087 -assets/KaTeX_Main-Regular-ypZvNtVU.ttf,1775251974218,b9391a3ced741fdbba0013271d3842116d7a5c9c9a3bae9b20f6fe727977e99b -assets/KaTeX_Main-Bold-waoOVXN0.ttf,1775251974218,7e68d925ddb291edef7974d261b1cd87add2590478448319fae5c676104faacc -assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2,1775251974218,f691829af7276fcfac11459e9a6cbc56b4eea702271a7b40c5852a17925186dd -assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2,1775251974218,e54368bcef9b645710c8daf3cb36f81fe33f02993b5e9bc33671927b85bcbf14 -assets/KaTeX_Fraktur-Bold-BsDP51OF.woff,1775251974218,c1aa25c0565ab36126dfa13e92fd9ccadb6eb007c14a312dc68bea89c9cee55c -assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2,1775251974218,b08538df27a2fc6207fb53e9db148214f6acd654049654be7e25a08e7442ba0e -assets/KaTeX_Fraktur-Regular-CB_wures.ttf,1775251974218,8515caa48dc361543f442a310a88ffca1fd344eabf71a7c67c86c127e660abd8 -assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff,1775251974217,296f1157aa34531f08b2bf52b0109202b341697fb9d5c39a96338f563a88ffae -assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf,1775251974218,a2e1ee3c5b4752d29122aa186e4240e4ee1e39dd89a1882290b4c77a8985ca30 -assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2,1775251974217,d62759b3f535a046de927c29aac9b08eed7627ae2f1c1bf14a4fdb9c0fb4be24 -assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff,1775251974217,0728c87ff4b5d7c239a6357f7461d3dda63121350aabfc85bcfae5fa2f209e11 -assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf,1775251974218,cc8cbd167150c02c5f4d482b407df458287feedac0833019f8d98ee7f2885f7f -assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf,1775251974217,05664e0978adafa697b3cc8dd8ec06536f505a89b843b46da8fbaea2be0602b9 -assets/KaTeX_AMS-Regular-BQhdFMY1.woff2,1775251974217,bff634555d2205a3b91a0a8dd4aa0dbf95ce890d2651fd63c20df1e2cc0739ba -assets/KaTeX_AMS-Regular-DMm9YOAa.woff,1775251974217,c9e0f0a1d5d9e1833dab92b05c6998adf535d677d370381f17991ea74f3f052b -assets/KaTeX_AMS-Regular-DRggAlZN.ttf,1775251974217,7b2569e8bc01101697d6f87bef599fe357e801b6297c5ff9c17475727fc55fbe -assets/index-QLeRNWpQ.js,1775251974216,7170de8d6b9ec7de5783c7b0415256c2c99cbce644f9d0fd71e51ee7c61f0798 diff --git a/whiteboard-dashboard/.firebaserc b/whiteboard-dashboard/.firebaserc deleted file mode 100644 index 0906889..0000000 --- a/whiteboard-dashboard/.firebaserc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": { - "default": "cs598-project-492002" - } -} diff --git a/whiteboard-dashboard/eslint.config.js b/whiteboard-dashboard/eslint.config.js index 4fa125d..3404bcd 100644 --- a/whiteboard-dashboard/eslint.config.js +++ b/whiteboard-dashboard/eslint.config.js @@ -1,15 +1,18 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import { defineConfig, globalIgnores } from 'eslint/config' +// ESLint flat config — lints TypeScript/React files with hooks and HMR rules +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{js,jsx}'], + files: ["**/*.{ts,tsx}"], extends: [ js.configs.recommended, + ...tseslint.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite, ], @@ -17,13 +20,13 @@ export default defineConfig([ ecmaVersion: 2020, globals: globals.browser, parserOptions: { - ecmaVersion: 'latest', + ecmaVersion: "latest", ecmaFeatures: { jsx: true }, - sourceType: 'module', + sourceType: "module", }, }, rules: { - 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + "@typescript-eslint/no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }], }, }, -]) +]); diff --git a/whiteboard-dashboard/firebase.json b/whiteboard-dashboard/firebase.json deleted file mode 100644 index 2c33c29..0000000 --- a/whiteboard-dashboard/firebase.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "hosting": { - "public": "dist", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - } -} diff --git a/whiteboard-dashboard/index.html b/whiteboard-dashboard/index.html index 148fca1..2180745 100644 --- a/whiteboard-dashboard/index.html +++ b/whiteboard-dashboard/index.html @@ -8,6 +8,6 @@
- + diff --git a/whiteboard-dashboard/package-lock.json b/whiteboard-dashboard/package-lock.json index 53fa71d..6d9bd39 100644 --- a/whiteboard-dashboard/package-lock.json +++ b/whiteboard-dashboard/package-lock.json @@ -13,23 +13,93 @@ "katex": "^0.16.44", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-katex": "^3.1.0" + "react-katex": "^3.1.0", + "react-markdown": "^10.1.0", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0" }, "devDependencies": { "@eslint/js": "^9.39.4", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "@types/d3-force": "^3.0.10", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.65.0", + "@typescript-eslint/parser": "^8.65.0", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.27", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "jsdom": "^29.1.1", "postcss": "^8.5.8", "tailwindcss": "^4.2.2", - "vite": "^8.0.1" + "typescript": "^6.0.3", + "typescript-eslint": "^8.65.0", + "vite": "^8.0.1", + "vitest": "^4.1.10" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -222,6 +292,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -270,6 +350,159 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", @@ -464,6 +697,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -875,6 +1126,92 @@ "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", + "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -886,6 +1223,25 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", @@ -901,6 +1257,13 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "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", @@ -935,13 +1298,46 @@ "@types/d3-selection": "*" } }, + "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/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "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==", - "dev": true, "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.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "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", @@ -949,11 +1345,31 @@ "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/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -969,42 +1385,462 @@ "@types/react": "^19.2.0" } }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "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/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@xyflow/react": { - "version": "12.10.2", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", - "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, "license": "MIT", - "dependencies": { - "@xyflow/system": "0.0.76", - "classcat": "^5.0.3", - "zustand": "^4.4.0" - }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.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.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "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.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.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.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xyflow/react": { + "version": "12.10.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", + "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.76", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" @@ -1067,6 +1903,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1090,12 +1937,32 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/autoprefixer": { - "version": "10.4.27", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", - "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, - "funding": [ + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ { "type": "opencollective", "url": "https://opencollective.com/postcss/" @@ -1127,6 +1994,16 @@ "postcss": "^8.1.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", @@ -1147,6 +2024,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", @@ -1223,6 +2110,26 @@ ], "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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1240,6 +2147,46 @@ "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/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", @@ -1266,6 +2213,16 @@ "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", @@ -1304,11 +2261,31 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "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==", - "devOptional": true, "license": "MIT" }, "node_modules/d3-color": { @@ -1439,11 +2416,24 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1457,6 +2447,26 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "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", @@ -1464,6 +2474,15 @@ "dev": true, "license": "MIT" }, + "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", @@ -1474,6 +2493,27 @@ "node": ">=8" } }, + "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/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/electron-to-chromium": { "version": "1.5.329", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.329.tgz", @@ -1481,6 +2521,25 @@ "dev": true, "license": "ISC" }, + "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-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1678,6 +2737,26 @@ "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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1688,6 +2767,22 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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", @@ -1853,6 +2948,174 @@ "node": ">=8" } }, + "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", @@ -1870,6 +3133,29 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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", @@ -1907,29 +3193,108 @@ "node": ">=0.8.19" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "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, + "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/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", - "dependencies": { - "is-extglob": "^2.1.1" - }, + "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-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-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-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-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1956,6 +3321,83 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2339,6 +3781,16 @@ "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", @@ -2362,181 +3814,909 @@ "yallist": "^3.0.2" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" } }, - "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==", + "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" + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "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", - "bin": { - "nanoid": "bin/nanoid.cjs" + "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" }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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/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/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==", + "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", - "peer": true, - "engines": { - "node": ">=0.10.0" + "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/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "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": { - "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" + "@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" }, - "engines": { - "node": ">= 0.8.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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, + "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": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" + "@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": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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, + "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": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" + "@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": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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, + "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": { - "callsites": "^3.0.0" + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" }, - "engines": { - "node": ">=6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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, + "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", - "engines": { - "node": ">=8" + "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/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, + "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", - "engines": { - "node": ">=8" + "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/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "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", - "engines": { - "node": ">=12" + "dependencies": { + "@types/mdast": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "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-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/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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/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/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==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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/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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.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", @@ -2574,6 +4754,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2586,6 +4804,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "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", @@ -2596,45 +4824,164 @@ "node": ">=6" } }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "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", + "peer": true + }, + "node_modules/react-katex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-katex/-/react-katex-3.1.0.tgz", + "integrity": "sha512-At9uLOkC75gwn2N+ZXc5HD8TlATsB+3Hkp9OGs6uA8tM3dwZ3Wljn74Bk3JyHFPgSnesY/EMrIAB1WJwqZqejA==", + "license": "MIT", + "dependencies": { + "katex": "^0.16.0" + }, + "peerDependencies": { + "prop-types": "^15.8.1", + "react": ">=15.3.2 <20" + } + }, + "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/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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", - "engines": { - "node": ">=0.10.0" + "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/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "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": { - "scheduler": "^0.27.0" + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" }, - "peerDependencies": { - "react": "^19.2.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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==", + "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", - "peer": true + "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/react-katex": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/react-katex/-/react-katex-3.1.0.tgz", - "integrity": "sha512-At9uLOkC75gwn2N+ZXc5HD8TlATsB+3Hkp9OGs6uA8tM3dwZ3Wljn74Bk3JyHFPgSnesY/EMrIAB1WJwqZqejA==", + "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": { - "katex": "^0.16.0" + "@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" }, - "peerDependencies": { - "prop-types": "^15.8.1", - "react": ">=15.3.2 <20" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, "node_modules/resolve-from": { @@ -2688,124 +5035,489 @@ "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "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==", + "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/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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "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-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/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/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } + "license": "0BSD", + "optional": true }, - "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==", + "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": { - "shebang-regex": "^3.0.0" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">=8" + "node": ">= 0.8.0" } }, - "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==", + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=8" + "node": ">=14.17" } }, - "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==", + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, "engines": { - "node": ">=0.10.0" + "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/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==", + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=20.18.1" + } + }, + "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": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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, + "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": { - "has-flag": "^4.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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/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/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, + "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": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "@types/unist": "^3.0.0" }, - "engines": { - "node": ">=12.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": { - "url": "https://github.com/sponsors/SuperchupuDev" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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==", - "dev": true, - "license": "0BSD", - "optional": true + "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/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, + "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": { - "prelude-ls": "^1.2.1" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, - "engines": { - "node": ">= 0.8.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/update-browserslist-db": { @@ -2858,6 +5570,48 @@ "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/vite": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", @@ -2936,6 +5690,154 @@ } } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2952,6 +5854,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -2962,6 +5881,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -3032,6 +5968,16 @@ "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/whiteboard-dashboard/package.json b/whiteboard-dashboard/package.json index c8fa9ba..c615b6b 100644 --- a/whiteboard-dashboard/package.json +++ b/whiteboard-dashboard/package.json @@ -7,6 +7,8 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run", "preview": "vite preview" }, "dependencies": { @@ -15,20 +17,32 @@ "katex": "^0.16.44", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-katex": "^3.1.0" + "react-katex": "^3.1.0", + "react-markdown": "^10.1.0", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0" }, "devDependencies": { "@eslint/js": "^9.39.4", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "@types/d3-force": "^3.0.10", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.65.0", + "@typescript-eslint/parser": "^8.65.0", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.27", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "jsdom": "^29.1.1", "postcss": "^8.5.8", "tailwindcss": "^4.2.2", - "vite": "^8.0.1" + "typescript": "^6.0.3", + "typescript-eslint": "^8.65.0", + "vite": "^8.0.1", + "vitest": "^4.1.10" } } diff --git a/whiteboard-dashboard/src/App.jsx b/whiteboard-dashboard/src/App.tsx similarity index 50% rename from whiteboard-dashboard/src/App.jsx rename to whiteboard-dashboard/src/App.tsx index a4b2882..8b6e8dd 100644 --- a/whiteboard-dashboard/src/App.jsx +++ b/whiteboard-dashboard/src/App.tsx @@ -1,44 +1,52 @@ -import React, { useEffect, useState, useRef } from "react"; +// Main application component — manages the ReactFlow graph, WebSocket connection, +// d3-force physics layout, conversation history, and user input. +import { useEffect, useState, useRef } from "react"; import { ReactFlow, useNodesState, useEdgesState, addEdge, Background, + BackgroundVariant, Controls, MarkerType, useReactFlow, ReactFlowProvider, + type Node, } from "@xyflow/react"; -import { forceSimulation, forceManyBody, forceCollide, forceX, forceY } from "d3-force"; +import { forceSimulation, forceManyBody, forceCollide, forceX, forceY, type SimulationNodeDatum } from "d3-force"; import MathNode from "./MathNode"; import "@xyflow/react/dist/style.css"; +import type { ReasoningNode, HistoryEntry } from "./types"; -const nodeTypes = { mathNode: MathNode }; +interface D3Node extends SimulationNodeDatum { + id: string; + x: number; + y: number; +} + +const nodeTypes = { mathNode: MathNode } as const; function FlowBoard() { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); - const [socket, setSocket] = useState(null); + const [input, setInput] = useState(""); const [isThinking, setIsThinking] = useState(false); - const [history, setHistory] = useState([]); - const [activePromptId, setActivePromptId] = useState(null); - const activeIdRef = useRef(null); - const socketRef = useRef(null); - - + const [history, setHistory] = useState([]); + const [editingId, setEditingId] = useState(null); + const [editText, setEditText] = useState(""); + const socketRef = useRef(null); const { setCenter } = useReactFlow(); - const currentNodes = nodes.filter((n) => n.data.promptId === activePromptId); - const nodeCount = currentNodes.length; - const hasFinalAnswer = currentNodes.some((n) => n.data.type === 'FINAL ANSWER'); + const nodeCount = nodes.length; + const hasFinalAnswer = nodes.some((n) => (n.data as Record).type === "FINAL ANSWER"); const progressPercentage = hasFinalAnswer ? 100 : Math.min((nodeCount / (nodeCount + 2)) * 100, 95); - const handleJumpToStep = (nodeId) => { + const handleJumpToStep = (nodeId: string) => { if (!nodeId) return; const targetNode = nodes.find((n) => n.id === nodeId); if (targetNode) { @@ -46,8 +54,7 @@ function FlowBoard() { } }; - // Stable ref so probe callbacks never go stale inside node data - const handleProbe = useRef((nodeId, nodeContent) => { + const handleProbe = useRef((nodeId: string, nodeContent: string) => { const ws = socketRef.current; if (ws?.readyState === WebSocket.OPEN) { setIsThinking(true); @@ -55,25 +62,39 @@ function FlowBoard() { } }); - const makeNode = (data, position, promptId) => ({ + const handleCancel = () => { + const ws = socketRef.current; + if (ws?.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "cancel" })); + setIsThinking(false); + } + }; + + const handleResend = (text: string) => { + const ws = socketRef.current; + if (ws?.readyState === WebSocket.OPEN && text.trim()) { + setEditingId(null); + setIsThinking(true); + ws.send(text); + } + }; + + const makeNode = (data: ReasoningNode, position: { x: number; y: number }): Node => ({ id: String(data.id), type: "mathNode", position, - x: position.x, - y: position.y, data: { label: data.label, math: data.content || "", type: data.node_type, - promptId, - onProbe: (nodeId, nodeContent) => handleProbe.current(nodeId, nodeContent), + onProbe: (nodeId: string, nodeContent: string) => handleProbe.current(nodeId, nodeContent), }, }); - // 1. Physics Engine useEffect(() => { if (nodes.length === 0) return; - const sim = forceSimulation(nodes) + const simNodes = nodes as unknown as D3Node[]; + const sim = forceSimulation(simNodes) .force("charge", forceManyBody().strength(-150)) .force("collision", forceCollide().radius(300)) .force("y", forceY(window.innerHeight / 2).strength(0.8)) @@ -82,31 +103,35 @@ function FlowBoard() { .alpha(0.1) .on("tick", () => { setNodes((nds) => - nds.map((node) => ({ ...node, position: { x: node.x, y: node.y } })) + nds.map((node) => ({ ...node, position: { x: (node as unknown as D3Node).x, y: (node as unknown as D3Node).y } })) ); }); - return () => sim.stop(); + return () => { sim.stop(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodes.length, setNodes]); - // 2. WebSocket useEffect(() => { const ws = new WebSocket("wss://professor-backend-656601378878.us-central1.run.app/ws/reason"); socketRef.current = ws; - setSocket(ws); - ws.onmessage = (event) => { - setIsThinking(false); + ws.onmessage = (event: MessageEvent) => { const data = JSON.parse(event.data); - const currentPromptId = activeIdRef.current; + + if (data.type === "cancelled") { + setIsThinking(false); + return; + } + + setIsThinking(false); setNodes((nds) => { const parentNode = nds.find((n) => String(n.id) === String(data.parent_id)); - const isAlternative = data.node_type === 'Alternative'; + const isAlternative = data.node_type === "Alternative"; const position = { x: parentNode ? parentNode.position.x + (isAlternative ? 0 : 550) : 100, y: parentNode ? parentNode.position.y + (isAlternative ? 220 : 0) : window.innerHeight / 2, }; - return nds.concat(makeNode(data, position, currentPromptId)); + return nds.concat(makeNode(data, position)); }); if (data.parent_id) { @@ -119,7 +144,6 @@ function FlowBoard() { animated: true, style: { stroke: "#3b82f6", strokeWidth: 3 }, markerEnd: { type: MarkerType.ArrowClosed, color: "#3b82f6" }, - data: { promptId: activeIdRef.current }, }, eds) ); } @@ -131,10 +155,7 @@ function FlowBoard() { const handleSend = () => { const ws = socketRef.current; if (ws?.readyState === WebSocket.OPEN && input.trim()) { - const newId = Date.now(); - activeIdRef.current = newId; - setHistory((prev) => [{ text: input, id: newId }, ...prev]); - setActivePromptId(newId); + setHistory((prev) => [{ text: input, id: Date.now() }, ...prev]); setIsThinking(true); ws.send(input); setInput(""); @@ -144,25 +165,23 @@ function FlowBoard() { return (
- {/* Progress Bar */} -
-
+
+
- {/* Dropdown HUD */} -
- GO TO: - handleJumpToStep(e.target.value)} style={{ border: "none", background: "#f1f5f9", borderRadius: "6px", padding: "4px 8px", fontSize: "12px", fontWeight: "bold", color: "#1e293b", outline: "none", cursor: "pointer" }}> - {currentNodes.map((node, index) => ( - + {nodes.map((node, index) => ( + ))}
e.data?.promptId === activePromptId)} + nodes={nodes} + edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} @@ -171,29 +190,60 @@ function FlowBoard() { translateExtent={[[-1000, -1000], [5000, 5000]]} fitView={false} > - + - {/* History Sidebar */}
Conversation History
{history.map((item) => ( -
{ setActivePromptId(item.id); activeIdRef.current = item.id; }} style={{ padding: "12px", borderRadius: "8px", fontSize: "14px", cursor: "pointer", background: activePromptId === item.id ? "#eff6ff" : "white", border: activePromptId === item.id ? "2px solid #3b82f6" : "1px solid #f1f5f9" }}> - {item.text.substring(0, 40)}{item.text.length > 40 ? '...' : ''} +
+ {editingId === item.id ? ( +
+ setEditText(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleResend(editText)} + style={{ padding: "6px", borderRadius: "4px", border: "1px solid #cbd5e1", fontSize: "13px" }} + autoFocus + /> +
+ + +
+
+ ) : ( +
+ { setEditingId(item.id); setEditText(item.text); }}> + {item.text.substring(0, 40)}{item.text.length > 40 ? "..." : ""} + + +
+ )}
))}
- {/* Thinking Status */} {isThinking && (
Professor is thinking... +
)} - {/* Input */}
setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSend()} placeholder="Ask a STEM problem..." style={{ width: "500px", padding: "12px", borderRadius: "8px", border: "1px solid #cbd5e1", color: "#0f172a" }} /> @@ -208,4 +258,4 @@ export default function App() { ); -} \ No newline at end of file +} diff --git a/whiteboard-dashboard/src/MathNode.jsx b/whiteboard-dashboard/src/MathNode.jsx deleted file mode 100644 index 0d518c6..0000000 --- a/whiteboard-dashboard/src/MathNode.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import 'katex/dist/katex.min.css'; -import React from 'react'; -import { Handle, Position } from '@xyflow/react'; -import ReactMarkdown from 'react-markdown'; -import remarkMath from 'remark-math'; -import rehypeKatex from 'rehype-katex'; - -const MathNode = ({ id, data }) => { - const typeStyles = { - 'Given': { border: '#94a3b8', bg: '#f8fafc' }, - 'Self-Correction': { border: '#ef4444', bg: '#fef2f2' }, - 'Alternative': { border: '#a855f7', bg: '#faf5ff' }, - 'Derivation': { border: '#3b82f6', bg: '#eff6ff' }, - 'Default': { border: '#10b981', bg: '#ffffff' }, - }; - const currentStyle = typeStyles[data.type] || typeStyles['Default']; - - return ( -
- - - {/* Header */} -
- - {data.type} - - {data.label} -
- - {/* Content */} -
- ( -

- ), - }} - > - {data.math} - -

- - {/* Probe button */} - {data.onProbe && ( - - )} - - -
- ); -}; - -export default MathNode; \ No newline at end of file diff --git a/whiteboard-dashboard/src/MathNode.tsx b/whiteboard-dashboard/src/MathNode.tsx new file mode 100644 index 0000000..cfb074b --- /dev/null +++ b/whiteboard-dashboard/src/MathNode.tsx @@ -0,0 +1,106 @@ +// Custom ReactFlow node that renders reasoning steps with KaTeX math. +// Displays a color-coded card with the node type, label, LaTeX content, +// and a probe button to ask for alternative explanations. +import "katex/dist/katex.min.css"; +import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; +import ReactMarkdown from "react-markdown"; +import remarkMath from "remark-math"; +import rehypeKatex from "rehype-katex"; + +// Data shape expected by this custom node type +export interface MathNodeData { + label: string; + math: string; + type: string; + onProbe?: (id: string, math: string) => void; + [key: string]: unknown; +} + +export type MathNodeType = Node; + +// Color scheme for each node type +const typeStyles: Record = { + "Given": { border: "#94a3b8", bg: "#f8fafc" }, + "Self-Correction": { border: "#ef4444", bg: "#fef2f2" }, + "Alternative": { border: "#a855f7", bg: "#faf5ff" }, + "Derivation": { border: "#3b82f6", bg: "#eff6ff" }, +}; + +const MathNode = ({ id, data }: NodeProps) => { + const currentStyle = typeStyles[data.type] || { border: "#10b981", bg: "#ffffff" }; + + return ( +
+ {/* Left handle — incoming edge from parent node */} + + + {/* Header row: node type badge + label */} +
+ + {data.type} + + {data.label} +
+ + {/* LaTeX math content rendered via KaTeX */} +
+ ( +

+ ), + }} + > + {data.math} + +

+ + {/* Probe button — asks the AI for an alternative explanation of this step */} + {data.onProbe && ( + + )} + + {/* Right handle — outgoing edge to child nodes */} + +
+ ); +}; + +export default MathNode; diff --git a/whiteboard-dashboard/src/__tests__/MathNode.test.tsx b/whiteboard-dashboard/src/__tests__/MathNode.test.tsx new file mode 100644 index 0000000..6ffd0bd --- /dev/null +++ b/whiteboard-dashboard/src/__tests__/MathNode.test.tsx @@ -0,0 +1,75 @@ +// Tests for the MathNode custom ReactFlow component — rendering, styling, and probe button behavior. + +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ReactFlowProvider } from '@xyflow/react'; +import MathNode, { type MathNodeType } from '../MathNode'; + +const mockData = { + label: 'Step 1', + math: '$x = 5$', + type: 'Given', + onProbe: () => {}, +}; + +const mockNode: MathNodeType = { + id: 'test-node', + type: 'mathNode', + position: { x: 0, y: 0 }, + data: mockData, + selected: false, + dragging: false, + draggable: true, + selectable: true, + deletable: true, + zIndex: 0, +}; + +function Wrapper({ node }: { node: MathNodeType }) { + return ( + + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + + + ); +} + +describe('MathNode', () => { + it('renders the node type badge', () => { + render(); + expect(screen.getByText('Given')).toBeInTheDocument(); + }); + + it('renders the label', () => { + render(); + expect(screen.getByText('Step 1')).toBeInTheDocument(); + }); + + it('renders LaTeX math content', () => { + const { container } = render(); + expect(container.textContent).toContain('x'); + expect(container.textContent).toContain('5'); + }); + + it('shows probe button when onProbe is provided', () => { + render(); + expect(screen.getByTitle('Ask about this step')).toBeInTheDocument(); + }); + + it('hides probe button when onProbe is undefined', () => { + render(); + expect(screen.queryByTitle('Ask about this step')).not.toBeInTheDocument(); + }); + + it('applies correct border color for Given type', () => { + render(); + const card = screen.getByText('Given').closest('div')?.parentElement; + expect(card).toHaveStyle('border: 2px solid #94a3b8'); + }); + + it('applies default style for unknown types', () => { + render(); + const card = screen.getByText('Unknown').closest('div')?.parentElement; + expect(card).toHaveStyle('border: 2px solid #10b981'); + }); +}); diff --git a/whiteboard-dashboard/src/__tests__/setup.ts b/whiteboard-dashboard/src/__tests__/setup.ts new file mode 100644 index 0000000..bb02c60 --- /dev/null +++ b/whiteboard-dashboard/src/__tests__/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest'; diff --git a/whiteboard-dashboard/src/main.jsx b/whiteboard-dashboard/src/main.jsx deleted file mode 100644 index b9a1a6d..0000000 --- a/whiteboard-dashboard/src/main.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.jsx' - -createRoot(document.getElementById('root')).render( - - - , -) diff --git a/whiteboard-dashboard/src/main.tsx b/whiteboard-dashboard/src/main.tsx new file mode 100644 index 0000000..4ef2ecf --- /dev/null +++ b/whiteboard-dashboard/src/main.tsx @@ -0,0 +1,11 @@ +// Entry point — mounts the React app into the DOM +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/whiteboard-dashboard/src/types.ts b/whiteboard-dashboard/src/types.ts new file mode 100644 index 0000000..22bd3c7 --- /dev/null +++ b/whiteboard-dashboard/src/types.ts @@ -0,0 +1,33 @@ +// Shared type definitions for the reasoning graph protocol between frontend and backend + +// The seven node types the AI can create +export type NodeType = + | "Given" + | "Objective" + | "Principle" + | "Derivation" + | "Self-Correction" + | "Alternative" + | "Final Answer"; + +// A node received from the backend WebSocket +export interface ReasoningNode { + id: string; + label: string; + content: string; + node_type: NodeType; + parent_id?: string; +} + +// Message sent when the user clicks the probe (?) button on a node +export interface ProbeMessage { + type: "probe"; + parent_id: string; + content: string; +} + +// An entry in the conversation history sidebar +export interface HistoryEntry { + text: string; + id: number; +} diff --git a/whiteboard-dashboard/src/vite-env.d.ts b/whiteboard-dashboard/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/whiteboard-dashboard/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/whiteboard-dashboard/tsconfig.json b/whiteboard-dashboard/tsconfig.json new file mode 100644 index 0000000..3e4897a --- /dev/null +++ b/whiteboard-dashboard/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/whiteboard-dashboard/vite.config.js b/whiteboard-dashboard/vite.config.js index 8b0f57b..0afbcfb 100644 --- a/whiteboard-dashboard/vite.config.js +++ b/whiteboard-dashboard/vite.config.js @@ -1,7 +1,18 @@ +// Vite configuration for the React frontend import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -// https://vite.dev/config/ export default defineConfig({ plugins: [react()], + build: { + rollupOptions: { + output: { + // Split large vendor libraries into separate chunks for faster loading + manualChunks(id) { + if (id.includes("@xyflow/react")) return "vendor-reactflow"; + if (id.includes("katex") || id.includes("react-markdown") || id.includes("remark-math") || id.includes("rehype-katex")) return "vendor-math"; + }, + }, + }, + }, }) diff --git a/whiteboard-dashboard/vite.config.ts b/whiteboard-dashboard/vite.config.ts new file mode 100644 index 0000000..1203c39 --- /dev/null +++ b/whiteboard-dashboard/vite.config.ts @@ -0,0 +1,23 @@ +/// +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./src/__tests__/setup.ts'], + css: true, + }, + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes("@xyflow/react")) return "vendor-reactflow"; + if (id.includes("katex") || id.includes("react-markdown") || id.includes("remark-math") || id.includes("rehype-katex")) return "vendor-math"; + }, + }, + }, + }, +})